diff --git a/.changeset/pvm-host-runtime-authority.md b/.changeset/pvm-host-runtime-authority.md new file mode 100644 index 000000000..17b272c02 --- /dev/null +++ b/.changeset/pvm-host-runtime-authority.md @@ -0,0 +1,8 @@ +--- +"@parity/truapi-host": minor +--- + +Add the product-scoped PolkaVM application runtime and its version-two guest transport. Route host capabilities through +runtime authority checks and preserve the existing consent requirements for signing and resource allocation. + +Keep the SDK usable in no-std guest builds, including Pocket message types. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5aeae1149..f1c2f915d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,6 +63,8 @@ jobs: - name: cargo build run: cargo build --workspace --all-targets --all-features + - name: cargo check no-std guest SDK + run: cargo check --locked -p truapi -p truapi-client --no-default-features - name: Check Rust library documentation env: RUSTDOCFLAGS: "-D warnings" diff --git a/CHANGELOG.md b/CHANGELOG.md index f8ff37d9c..5804e6fc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,38 @@ All notable changes to the TrUAPI protocol are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), generated from [Conventional Commits](https://www.conventionalcommits.org/). +## [Unreleased] + +### Changed + +- isolate the optional PolkaVM runtime in `truapi-polkavm-host`; keep the base + server and browser asset distribution independent, and pin cooperative update + deadlines, multi-touch input, and image clipboard output (#540) +- let browser signing hosts request personhood-backed Statement Store + allowances for products instead of reporting the allocator as native-only +- migrate the generic runtime and Rust client to SDK 0.16's scoped wire codec 2; + product guests must be rebuilt rather than sending codec-1 frames + +### Added + +- report local-wallet registration stages and retryable chain-read errors through + a request-scoped browser callback without resubmitting accepted claims +- expose local signing-wallet username registration and chain-verified identity + refresh through the browser worker, with native UID proofs and RFC-0004 + X25519 identifier keys +- Generate a transport-neutral `no_std` Rust client with typed request responses, + subscription interrupts, and host-initiated Renderer subscription codecs. +- Generate complete App, Widget, Worker, and Worker-only method catalogs from + the canonical protocol schema. + +### Fixed + +- keep host-backed allowance helpers available on Wasm with browser-compatible + polling clocks, while excluding the native-only renewal driver (#540) +- report the immutable PolkaVM runtime revision actually pinned by the optional + composition crate (#540) +- return a decode error instead of trapping when subscription helpers receive a + request descriptor ## [0.20.0] - 2026-09-23 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 5f6e078ca..84e7413b8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,6 +16,8 @@ rust/crates/ truapi-platform/ Host syscall traits (storage, navigation, consent, ...) truapi-provider/ network provider backends (WebSocket RPC or smoldot light-client) truapi-server/ Rust runtime hosts implement; ships as WASM (browser/node) + truapi-polkavm-host/ optional native composition pinned to + paritytech/polkavm-host-runtime truapi-host-cli/ CLI pairing/signing hosts; Bun scripts share the container web API gates js/packages/ truapi/ @parity/truapi TS package; generated TS lives under ignored paths @@ -80,6 +82,13 @@ scripts/cli-runner-package.test.ts documents intentionally unpublished npm package versions ``` +The PolkaVM runtime implementation, wire crates, browser package, and ABI +contract live in `paritytech/polkavm-host-runtime`. The base `truapi-server` +must remain PolkaVM-free. Native integrations use the optional +`truapi-polkavm-host` composition crate; browser integrations consume +`@parity/polkavm-browser-runtime` directly. Never copy or export browser assets +from this repository. + ### Crate + binding invariants - `truapi` is canonical; runtime crates re-export rather than redefine. New diff --git a/Cargo.lock b/Cargo.lock index c39f1b6d9..79ef94349 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,6 +45,15 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "1.0.0" @@ -126,7 +135,7 @@ dependencies = [ "objc2-foundation", "parking_lot", "percent-encoding", - "windows-sys 0.60.2", + "windows-sys 0.52.0", "x11rb", ] @@ -472,6 +481,15 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + [[package]] name = "askama" version = "0.16.0" @@ -497,7 +515,7 @@ dependencies = [ "memchr", "proc-macro2", "quote", - "rustc-hash", + "rustc-hash 2.1.3", "serde", "serde_derive", "syn 2.0.119", @@ -518,7 +536,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7db09fde9143e7ac4513358fb32ee32847125b63b18ea715afd487956da715da" dependencies = [ - "rustc-hash", + "rustc-hash 2.1.3", "serde", "serde_derive", "unicode-ident", @@ -719,6 +737,21 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitcoin_hashes" version = "0.14.101" @@ -728,11 +761,20 @@ dependencies = [ "hex-conservative", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] [[package]] name = "bitvec" @@ -776,6 +818,12 @@ dependencies = [ "constant_time_eq 0.4.2", ] +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.9.0" @@ -852,6 +900,20 @@ name = "bytemuck" version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] [[package]] name = "byteorder" @@ -1042,6 +1104,16 @@ dependencies = [ "error-code", ] +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width 0.1.14", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -1148,6 +1220,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -1164,6 +1246,17 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1231,7 +1324,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags", + "bitflags 2.13.1", "crossterm_winapi", "derive_more 2.1.1", "document-features", @@ -1479,7 +1572,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags", + "bitflags 2.13.1", "objc2", ] @@ -1586,7 +1679,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1621,7 +1714,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef975e30683b2d965054bb0a836f8973857c4ebf6acf274fe46617cd285060d8" dependencies = [ - "foldhash", + "foldhash 0.2.0", "libm", "portable-atomic", "siphasher", @@ -1708,12 +1801,45 @@ 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 = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1950,6 +2076,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -1959,7 +2097,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.1", "wasm-bindgen", ] @@ -1974,6 +2112,17 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + [[package]] name = "glob" version = "0.3.4" @@ -2026,6 +2175,27 @@ dependencies = [ "web-sys", ] +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + [[package]] name = "goblin" version = "0.8.2" @@ -2037,6 +2207,57 @@ dependencies = [ "scroll", ] +[[package]] +name = "gpu-alloc" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45cf04b2726f02df5508c6de726acdc90cdf97ac771a9a0ffd8ba10a6e696bf9" +dependencies = [ + "bitflags 2.13.1", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bbed164dd10ed526c2e4fe3e721ca4a71c61730e5aafac6844b417b3227058" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "gpu-allocator" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "windows", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.13.1", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.13.1", +] + [[package]] name = "h2" version = "0.4.15" @@ -2084,6 +2305,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", + "foldhash 0.1.5", ] [[package]] @@ -2094,7 +2316,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.2.0", "serde", "serde_core", ] @@ -2107,7 +2329,7 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.2.0", ] [[package]] @@ -2137,6 +2359,12 @@ dependencies = [ "arrayvec 0.7.8", ] +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + [[package]] name = "hkdf" version = "0.12.4" @@ -2666,7 +2894,7 @@ dependencies = [ "parking_lot", "pin-project", "rand 0.8.7", - "rustc-hash", + "rustc-hash 2.1.3", "serde", "serde_json", "thiserror 1.0.69", @@ -2769,6 +2997,23 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + [[package]] name = "konst" version = "0.2.20" @@ -2790,12 +3035,28 @@ 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.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libm" version = "0.2.16" @@ -2857,7 +3118,7 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e752191d037c44ad111a8caa762921926658402f01cc1253f7bef2020ece4f5e" dependencies = [ - "bitflags", + "bitflags 2.13.1", ] [[package]] @@ -2917,6 +3178,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "matchers" version = "0.2.0" @@ -2944,6 +3214,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "metal" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" +dependencies = [ + "bitflags 2.13.1", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + [[package]] name = "minicov" version = "0.3.8" @@ -2998,6 +3283,28 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "685a9ac4b61f4e728e1d2c6a7844609c16527aeb5e6c865915c08e619c16410f" +[[package]] +name = "naga" +version = "24.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e380993072e52eef724eddfcde0ed013b0c023c3f0417336ed041aa9f076994e" +dependencies = [ + "arrayvec 0.7.8", + "bit-set", + "bitflags 2.13.1", + "cfg_aliases", + "codespan-reporting", + "hexf-parse", + "indexmap", + "log", + "rustc-hash 1.1.0", + "spirv", + "strum 0.26.3", + "termcolor", + "thiserror 2.0.19", + "unicode-xid", +] + [[package]] name = "nanoid" version = "0.4.0" @@ -3007,6 +3314,15 @@ dependencies = [ "rand 0.8.7", ] +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys 0.3.1", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -3096,6 +3412,15 @@ dependencies = [ "libc", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + [[package]] name = "objc2" version = "0.6.4" @@ -3111,7 +3436,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags", + "bitflags 2.13.1", "objc2", "objc2-core-graphics", "objc2-foundation", @@ -3123,7 +3448,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags", + "bitflags 2.13.1", "dispatch2", "objc2", ] @@ -3134,7 +3459,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags", + "bitflags 2.13.1", "dispatch2", "objc2", "objc2-core-foundation", @@ -3153,7 +3478,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags", + "bitflags 2.13.1", "objc2", "objc2-core-foundation", ] @@ -3164,7 +3489,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags", + "bitflags 2.13.1", "objc2", "objc2-core-foundation", ] @@ -3199,6 +3524,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + [[package]] name = "palette" version = "0.7.7" @@ -3319,6 +3653,12 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "picosimd" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7823ffd00d2b55ebe51750a19f47f2a33cb1f1d135f5cba893379b81c4d44856" + [[package]] name = "pin-project" version = "1.1.13" @@ -3366,6 +3706,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + [[package]] name = "plain" version = "0.2.3" @@ -3378,7 +3724,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags", + "bitflags 2.13.1", "crc32fast", "fdeflate", "flate2", @@ -3386,36 +3732,114 @@ dependencies = [ ] [[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +name = "polkavm" +version = "0.37.0" +source = "git+https://github.com/paritytech/polkavm.git?rev=c160c13c3c29bf3219ce1404ec95976000235a94#c160c13c3c29bf3219ce1404ec95976000235a94" dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix", - "windows-sys 0.61.2", + "libc", + "log", + "picosimd", + "polkavm-assembler", + "polkavm-common", + "polkavm-linux-raw", ] [[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +name = "polkavm-assembler" +version = "0.37.0" +source = "git+https://github.com/paritytech/polkavm.git?rev=c160c13c3c29bf3219ce1404ec95976000235a94#c160c13c3c29bf3219ce1404ec95976000235a94" dependencies = [ - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", + "log", ] [[package]] -name = "portable-atomic" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" - +name = "polkavm-common" +version = "0.37.0" +source = "git+https://github.com/paritytech/polkavm.git?rev=c160c13c3c29bf3219ce1404ec95976000235a94#c160c13c3c29bf3219ce1404ec95976000235a94" +dependencies = [ + "log", + "picosimd", + "polkavm-assembler", +] + +[[package]] +name = "polkavm-gpu-wire" +version = "0.3.0" +source = "git+https://github.com/paritytech/polkavm-host-runtime.git?rev=e60a6135be6e00c72c90ca4abe23d2002f4eb962#e60a6135be6e00c72c90ca4abe23d2002f4eb962" + +[[package]] +name = "polkavm-host-runtime" +version = "0.3.0" +source = "git+https://github.com/paritytech/polkavm-host-runtime.git?rev=e60a6135be6e00c72c90ca4abe23d2002f4eb962#e60a6135be6e00c72c90ca4abe23d2002f4eb962" +dependencies = [ + "anyhow", + "getrandom 0.3.4", + "log", + "polkavm", + "polkavm-gpu-wire", + "polkavm-motion-wire", + "polkavm-ui-wire", + "pollster", + "serde", + "serde_json", + "thiserror 1.0.69", + "uniffi", + "wasm-encoder", + "wgpu", +] + +[[package]] +name = "polkavm-linux-raw" +version = "0.37.0" +source = "git+https://github.com/paritytech/polkavm.git?rev=c160c13c3c29bf3219ce1404ec95976000235a94#c160c13c3c29bf3219ce1404ec95976000235a94" + +[[package]] +name = "polkavm-motion-wire" +version = "0.3.0" +source = "git+https://github.com/paritytech/polkavm-host-runtime.git?rev=e60a6135be6e00c72c90ca4abe23d2002f4eb962#e60a6135be6e00c72c90ca4abe23d2002f4eb962" + +[[package]] +name = "polkavm-ui-wire" +version = "0.3.0" +source = "git+https://github.com/paritytech/polkavm-host-runtime.git?rev=e60a6135be6e00c72c90ca4abe23d2002f4eb962#e60a6135be6e00c72c90ca4abe23d2002f4eb962" + +[[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 = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" + [[package]] name = "potential_utf" version = "0.1.5" @@ -3440,6 +3864,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + [[package]] name = "primitive-types" version = "0.13.1" @@ -3493,6 +3923,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" + [[package]] name = "pxfm" version = "0.1.30" @@ -3522,7 +3958,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.3", "rustls", "socket2", "thiserror 2.0.19", @@ -3543,7 +3979,7 @@ dependencies = [ "rand 0.10.2", "rand_pcg", "ring", - "rustc-hash", + "rustc-hash 2.1.3", "rustls", "rustls-pki-types", "slab", @@ -3564,7 +4000,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3576,6 +4012,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -3644,6 +4086,12 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + [[package]] name = "ratatui" version = "0.30.2" @@ -3663,7 +4111,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" dependencies = [ - "bitflags", + "bitflags 2.13.1", "compact_str", "hashbrown 0.17.1", "itertools 0.14.0", @@ -3675,7 +4123,7 @@ dependencies = [ "thiserror 2.0.19", "unicode-segmentation", "unicode-truncate", - "unicode-width", + "unicode-width 0.2.2", ] [[package]] @@ -3696,7 +4144,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" dependencies = [ - "bitflags", + "bitflags 2.13.1", "hashbrown 0.17.1", "indoc", "instability", @@ -3707,9 +4155,15 @@ dependencies = [ "strum 0.28.0", "time", "unicode-segmentation", - "unicode-width", + "unicode-width 0.2.2", ] +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + [[package]] name = "rayon" version = "1.12.0" @@ -3736,7 +4190,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.1", ] [[package]] @@ -3756,6 +4210,12 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + [[package]] name = "reqwest" version = "0.12.28" @@ -3824,6 +4284,12 @@ dependencies = [ "lru 0.16.4", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.3" @@ -3851,11 +4317,11 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3901,7 +4367,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "jni", "log", @@ -3913,7 +4379,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs 0.26.11", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -4170,8 +4636,8 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags", - "core-foundation", + "bitflags 2.13.1", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -4414,6 +4880,15 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + [[package]] name = "smallstr" version = "0.3.1" @@ -4693,6 +5168,15 @@ version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.13.1", +] + [[package]] name = "spki" version = "0.7.3" @@ -4721,6 +5205,15 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros 0.26.4", +] + [[package]] name = "strum" version = "0.27.2" @@ -4739,6 +5232,19 @@ dependencies = [ "strum_macros 0.28.0", ] +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.119", +] + [[package]] name = "strum_macros" version = "0.27.2" @@ -5028,7 +5534,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5334,7 +5840,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags", + "bitflags 2.13.1", "bytes", "futures-util", "http", @@ -5433,6 +5939,14 @@ dependencies = [ "uniffi", ] +[[package]] +name = "truapi-client" +version = "0.16.0" +dependencies = [ + "parity-scale-codec", + "truapi", +] + [[package]] name = "truapi-codegen" version = "0.1.0" @@ -5487,7 +6001,7 @@ dependencies = [ "truapi", "truapi-platform", "truapi-server", - "unicode-width", + "unicode-width 0.2.2", "zeroize", ] @@ -5517,6 +6031,14 @@ dependencies = [ "zeroize", ] +[[package]] +name = "truapi-polkavm-host" +version = "0.2.0" +dependencies = [ + "polkavm-host-runtime", + "truapi-server", +] + [[package]] name = "truapi-provider" version = "0.1.0" @@ -5718,9 +6240,15 @@ checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" dependencies = [ "itertools 0.14.0", "unicode-segmentation", - "unicode-width", + "unicode-width 0.2.2", ] +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-width" version = "0.2.2" @@ -6015,6 +6543,15 @@ 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.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.126" @@ -6109,6 +6646,16 @@ version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c31d56021e873866c968588ed85ccdf56db5c426e44afdb4618c39895104b920" +[[package]] +name = "wasm-encoder" +version = "0.240.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06d642d8c5ecc083aafe9ceb32809276a304547a3a6eeecceb5d8152598bc71f" +dependencies = [ + "leb128fmt", + "wasmparser 0.240.0", +] + [[package]] name = "wasmi" version = "0.40.0" @@ -6122,7 +6669,7 @@ dependencies = [ "wasmi_collections", "wasmi_core", "wasmi_ir", - "wasmparser", + "wasmparser 0.221.3", ] [[package]] @@ -6156,7 +6703,18 @@ version = "0.221.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d06bfa36ab3ac2be0dee563380147a5b81ba10dd8885d7fbbc9eb574be67d185" dependencies = [ - "bitflags", + "bitflags 2.13.1", +] + +[[package]] +name = "wasmparser" +version = "0.240.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b722dcf61e0ea47440b53ff83ccb5df8efec57a69d150e4f24882e4eba7e24a4" +dependencies = [ + "bitflags 2.13.1", + "indexmap", + "semver", ] [[package]] @@ -6230,6 +6788,115 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +[[package]] +name = "wgpu" +version = "24.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0b3436f0729f6cdf2e6e9201f3d39dc95813fad61d826c1ed07918b4539353" +dependencies = [ + "arrayvec 0.7.8", + "bitflags 2.13.1", + "cfg_aliases", + "document-features", + "js-sys", + "log", + "naga", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "24.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f0aa306497a238d169b9dc70659105b4a096859a34894544ca81719242e1499" +dependencies = [ + "arrayvec 0.7.8", + "bit-vec", + "bitflags 2.13.1", + "cfg_aliases", + "document-features", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.19", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-hal" +version = "24.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112f464674ca69f3533248508ee30cb84c67cf06c25ff6800685f5e0294e259" +dependencies = [ + "android_system_properties", + "arrayvec 0.7.8", + "ash", + "bit-set", + "bitflags 2.13.1", + "block", + "bytemuck", + "cfg_aliases", + "core-graphics-types", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "metal", + "naga", + "ndk-sys", + "objc", + "once_cell", + "ordered-float", + "parking_lot", + "profiling", + "range-alloc", + "raw-window-handle", + "renderdoc-sys", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.19", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "windows", + "windows-core", +] + +[[package]] +name = "wgpu-types" +version = "24.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c" +dependencies = [ + "bitflags 2.13.1", + "js-sys", + "log", + "web-sys", +] + [[package]] name = "winapi" version = "0.3.9" @@ -6252,7 +6919,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6261,6 +6928,51 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -6268,39 +6980,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows-sys" -version = "0.45.0" +name = "windows-result" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" dependencies = [ - "windows-targets 0.42.2", + "windows-targets 0.52.6", ] [[package]] -name = "windows-sys" -version = "0.52.0" +name = "windows-strings" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ + "windows-result", "windows-targets 0.52.6", ] [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets 0.52.6", + "windows-targets 0.42.2", ] [[package]] name = "windows-sys" -version = "0.60.2" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.53.5", + "windows-targets 0.52.6", ] [[package]] @@ -6336,30 +7049,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -6372,12 +7068,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -6390,12 +7080,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[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.42.2" @@ -6408,24 +7092,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[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.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[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.42.2" @@ -6438,12 +7110,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[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.42.2" @@ -6456,12 +7122,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[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.42.2" @@ -6474,12 +7134,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[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.42.2" @@ -6492,12 +7146,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[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 = "1.0.4" @@ -6507,6 +7155,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "writeable" version = "0.6.3" @@ -6561,6 +7215,12 @@ dependencies = [ "rustix", ] +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + [[package]] name = "yap" version = "0.12.0" diff --git a/README.md b/README.md index 4f89bb1b4..0d070bd4a 100644 --- a/README.md +++ b/README.md @@ -90,11 +90,13 @@ including cancellation while the app is backgrounded. ``` rust/crates/ truapi/ Rust traits, versioned envelopes, and latest payload re-exports + truapi-client/ generated transport-neutral no-std client codecs and execution catalogs truapi-codegen/ rustdoc JSON to TypeScript client + Rust dispatcher truapi-macros/ TrUAPI wire annotations and inter-host SSO proc macros truapi-platform/ Host syscall traits used by truapi-server (storage, navigation, consent, ...) truapi-provider/ Network provider backends (WebSocket RPC or smoldot light-client) truapi-server/ Host runtime: dispatcher, typed SCALE logic, chain signing, WASM surface + truapi-polkavm-host/ Optional native composition of truapi-server and a pinned PolkaVM runtime js/packages/ truapi/ @parity/truapi TypeScript client truapi-host/ @parity/truapi-host: WASM-backed host runtime; entries `.` @@ -126,6 +128,14 @@ scripts/battery.sh Run the generated battery against both headless CLI h plus the Pocket phase a Worker execution serves ``` +The PolkaVM application runtime, GPU/UI wire contracts, and browser runtime +live in +[`paritytech/polkavm-host-runtime`](https://github.com/paritytech/polkavm-host-runtime). +Native hosts that need both runtimes link the optional `truapi-polkavm-host` +composition crate; the base `truapi-server` remains PolkaVM-free. Browser hosts +consume `@parity/polkavm-browser-runtime` directly; browser assets are not +shipped from this repository. + Taking a screenshot opens **Report app issue** wherever the shake-opened Debug menu is, which is every build except the store submission: `DEBUG_TOOLS_ENABLED` on Android, false only for the `release` build type, and `TESTNET_FEATURE` on @@ -215,8 +225,8 @@ control of quota and of whether the bytes are backed up or encrypted. ## How it works 1. The protocol is defined as Rust traits in [`rust/crates/truapi/`](rust/crates/truapi/), with each trait tagged `#[wire_trait(id = N)]` and each method tagged `#[wire(id = N)]` for a stable byte-level `(trait, method)` dispatch table. Every method's doc comment must carry a ` ```ts ` example, which codegen extracts into the playground's EXAMPLE tab; the build fails if any method is missing one. -2. `truapi-codegen` reads rustdoc JSON for that crate and generates the TypeScript client under git-ignored paths in `js/packages/truapi/`. -3. Higher-level SDKs wrap the typed client; the transport encodes SCALE frames and ships them over WebSocket, `MessagePort`, or `postMessage` in iframe mode to the host. +2. `truapi-codegen` reads rustdoc JSON for that crate and generates the TypeScript client under git-ignored paths in `js/packages/truapi/`, the Rust host dispatcher, and the transport-neutral `no_std` Rust client. The Rust client exports typed method markers plus complete App, Widget, Worker, and Worker-only catalogs from the same wire schema. +3. Higher-level SDKs wrap the generated client; each runtime provides only its native frame transport. Browser products use `MessagePort` (or `postMessage` in iframe mode), while sandboxed runtimes such as PolkaVM supply explicit host imports. 4. The host decodes the frame, dispatches to the matching trait method, encodes the response, and ships it back. Wire ids are append-only per trait: a trait id is never reassigned and a method id is never renumbered or reused within its trait, so deployed products stay compatible across protocol revisions. New methods take the next free method ids in their own trait and leave every other trait untouched. Trait 255 is permanently reserved for a correlated protocol error, allowing either peer to reject API messages introduced after it was released instead of leaving the caller pending. diff --git a/android/truapi-host/src/main/kotlin/io/parity/truapi/TrUAPIHost.kt b/android/truapi-host/src/main/kotlin/io/parity/truapi/TrUAPIHost.kt index 30cb574a4..4dc5c96f3 100644 --- a/android/truapi-host/src/main/kotlin/io/parity/truapi/TrUAPIHost.kt +++ b/android/truapi-host/src/main/kotlin/io/parity/truapi/TrUAPIHost.kt @@ -83,6 +83,7 @@ import uniffi.truapi_server.NativeRuntimeConfigException import uniffi.truapi_server.NativeStatementRenewalTarget import uniffi.truapi_server.NativeTrackedStatementRenewalTarget import uniffi.truapi_server.StatementRenewalReport +import uniffi.truapi_server.SsoRequestOutcome import uniffi.truapi_server.WorkerTransition import uniffi.truapi_server.WsBridgeEndpoint import uniffi.truapi_server.WsBridgeStartException @@ -895,6 +896,14 @@ class TrUAPIHostRuntime private constructor( inner.activateLocalSession(secret, liteUsername) } + /** Handle a paired session's opaque SSO request, awaiting native user consent. */ + @Throws(HostRejection::class) + suspend fun handleSsoRequest(message: ByteArray): SsoRequestOutcome = + inner.handleSsoRequest(message) + + /** Encode the disconnect notification; the caller routes it to the ending session. */ + fun prepareDisconnectRequest(): ByteArray = inner.prepareDisconnectRequest() + /** Push a JSON-RPC response from a native chain connection into the runtime. */ fun notifyChainResponse(connectionId: UInt, json: String) { inner.notifyChainResponse(connectionId, json) diff --git a/deny.toml b/deny.toml index 1a454ec77..e76a7185f 100644 --- a/deny.toml +++ b/deny.toml @@ -41,6 +41,12 @@ exceptions = [ # BSL-1.0 is permissive, but scope it so it cannot spread to other crates. { name = "clipboard-win", allow = ["BSL-1.0"] }, { name = "error-code", allow = ["BSL-1.0"] }, + # The host-neutral PolkaVM runtime and wire protocols are intentionally + # MPL-2.0 so their Rust implementations retain file-level copyleft. + { name = "polkavm-gpu-wire", allow = ["MPL-2.0"] }, + { name = "polkavm-motion-wire", allow = ["MPL-2.0"] }, + { name = "polkavm-ui-wire", allow = ["MPL-2.0"] }, + { name = "polkavm-host-runtime", allow = ["MPL-2.0"] }, { name = "uniffi", allow = ["MPL-2.0"] }, { name = "uniffi_bindgen", allow = ["MPL-2.0"] }, { name = "uniffi_checksum_derive", allow = ["MPL-2.0"] }, diff --git a/docs/issue-drafts/pointer-capture-control.md b/docs/issue-drafts/pointer-capture-control.md new file mode 100644 index 000000000..509573f68 --- /dev/null +++ b/docs/issue-drafts/pointer-capture-control.md @@ -0,0 +1,146 @@ +# Add host-mediated pointer capture to TrUAPI + +## Status + +Parked prototype note. Do not start an RFC or implementation from this draft +yet. Current hosts capture non-Tri2D surfaces after a primary click, which is +sufficient for the existing game prototypes. Revisit only after prototypes +demonstrate that products need runtime capture/release control beyond the +host's click-to-capture and Escape-to-release policy. + +## Summary + +Pointer lock is host functionality. A product should request and release it through TrUAPI rather than declaring a separate `relative-pointer` feature in its manifest. The host owns the application surface, enforces user activation, calls the platform pointer-lock API, and reports lifecycle changes. High-frequency movement remains on the bounded device-input data path. + +This separates three concerns: + +- The App manifest declares ordinary `pointer` input support. +- TrUAPI controls whether pointer lock is free, armed, or captured. +- Request denial is a call error, and an older host reports the protocol's + standard unsupported-method error. +- The runtime input ABI delivers absolute positions while free and relative + deltas while captured. + +## Motivation + +Graphics profile is not a pointer policy. Some framebuffer and WebGPU products are first-person games that need unbounded relative motion; others are cursor-driven tools that must leave the system pointer free. Defaulting all non-Tri2D surfaces to pointer lock surprises cursor-driven products, while a static manifest feature cannot express when a mixed menu/game product wants capture. + +Pointer lock also cannot be delegated to sandboxed product code. Browser and native hosts own the trusted surface and must preserve an unconditional escape path. + +## Proposed control plane + +Names are placeholders for the RFC: + +```ts +input.pointerCapture.request(): Promise> + +input.pointerCapture.release(): Promise> + +input.pointerCapture.subscribe(): Subscription< + \"free\" | \"armed\" | \"captured\" +> +``` + +The subscription follows the existing `theme.subscribe()` and +`locale.subscribe()` convention: it emits the current state immediately and +then every transition. A separate `status()` request would duplicate the first +subscription item. + +A browser generally cannot honor `request()` immediately because pointer lock +requires a transient user activation. In that case `request()` returns +`armed`; the host acquires lock on the next eligible primary click on the +requesting product's active surface. + +`release()` is idempotent and always permitted. Escape always releases capture +and is consumed by the host for that transition rather than forwarded to the +product as a key press. + +`denied` is a request error rather than a durable capture state: after denial +the surface remains `free`. A host that predates these methods already has a +standard answer—the TrUAPI protocol-error frame for an unknown discriminant—so +`unsupported` does not belong in the domain state enum. + +## Fit with the existing TrUAPI architecture + +The API should be a small `Input` method group, backed by an optional +`PointerCaptureHost` platform callback. The TrUAPI server is product-scoped, so +the callback already runs in the identity and lifecycle context of the calling +product. Web, desktop, and other surface-owning hosts implement the callback; +CLI or non-pointer hosts omit it and receive the existing `Unsupported` +behavior. + +This should not be added to `HostDevicePermissionRequest`. RFC 0002 defines +device permission decisions as indefinitely persisted grants or denials, often +paired with an operating-system permission. Pointer lock is transient surface +state, requires a current user gesture, and must always be releasable. A future +host policy may gate the request, but the first API does not need a persisted +permission. + +The RFC must allocate new append-only wire discriminants for request, release, +and the subscription lifecycle. Existing discriminants cannot be reused. + +## Input data plane + +Pointer movement does not travel through TrUAPI. The existing bounded runtime input channel remains the data plane: + +- Free pointer: event type `5`, absolute canvas `x`/`y`. +- Captured pointer: event type `6`, signed relative `dx`/`dy`. +- Buttons: event types `3` and `4` in either state. + +Hosts should discard the first cursor-warp sample after lock acquisition, reject non-finite or implausible discontinuities, and coalesce high-rate deltas to the runtime's bounded update cadence. + +The browser operation is Pointer Lock (`requestPointerLock()`), not DOM pointer capture (`setPointerCapture()`). DOM pointer capture only preserves event routing during a drag and does not provide an unbounded relative pointer. + +## Host invariants + +- Pointer lock defaults to off for every graphics profile. +- Only the product bound to the active verified surface may request capture. +- Capture requires a real user gesture on that surface. +- A product cannot suppress Escape release. +- Losing focus, replacing the product, navigating, or destroying the surface releases capture. +- Relative events are emitted only while the surface actually owns pointer lock. +- Request denial and every state transition are observable by the product so it + can stop mouse-look or present fallback controls. +- Hosts that predate the API return TrUAPI's standard unsupported-method error; + products decide whether keyboard, touch, or controller input is an acceptable + fallback. + +## Manifest relationship + +No separate `relative-pointer` manifest feature is required. TrUAPI already +provides generic unsupported-method handling, the runtime request expresses +current intent, and the host remains the authority. + +A future RFC may revisit install-time metadata if hosts need to warn that a product cannot function without pointer lock. Such metadata would describe compatibility or user experience; it would not replace runtime request/release control. + +## Future user policy + +A host may eventually expose a per-product policy such as `Ask`, `Allow`, or `Block`, plus a global `Never allow pointer lock` setting. This is intentionally deferred. + +If added, the setting should authorize or deny product requests; it should not force an arbitrary cursor-driven product into relative mode. The policy should be scoped to a verified product identity, show a visible capture indicator, and preserve Escape as an unconditional release path. + +## Revisit criteria + +Keep the existing host behavior while prototypes mature. Promote this note to +an RFC only when at least one real product demonstrates a concrete need such +as: + +- a cursor-driven framebuffer or WebGPU surface that must never capture; +- a mixed menu/game product that must release and reacquire capture without + relying on Escape and a new click; +- a cross-host inconsistency that cannot be fixed as host policy; +- a product that needs to observe capture denial or loss to provide a usable + fallback. + +If one of those cases appears, the RFC should specify request, release, the +immediate state subscription, wire ids, user-gesture semantics, and errors. +Only after that should the shared Rust core, host callbacks, Epoca, Dotli, and +products be changed. + +## Unresolved questions + +- Whether `request()` should be a one-shot request or remain armed across Escape release until the product cancels it. +- Whether capture permission persists per product, per publisher, per artifact version, or only for the current session. +- Whether native hosts need a distinct result for operating-system policy denial. +- Whether products need an optional install-time declaration that pointer lock is essential rather than merely supported. +- Whether a host should expose a standard visual indicator or leave presentation to each host implementation. diff --git a/js/packages/truapi-host/CHANGELOG.md b/js/packages/truapi-host/CHANGELOG.md index 96c5c2f11..dfce791c2 100644 --- a/js/packages/truapi-host/CHANGELOG.md +++ b/js/packages/truapi-host/CHANGELOG.md @@ -1,5 +1,12 @@ # @parity/truapi-host +## Unreleased + +### Patch Changes + +- Keep monitoring accepted Lite username claims until chain ownership is confirmed + or the wallet activation is disposed. Recover from transient chain-read failures + without resubmitting registration or reporting a fixed polling cutoff as failure. ## 0.20.0 ### Patch Changes diff --git a/js/packages/truapi-host/README.md b/js/packages/truapi-host/README.md index a069eca7e..bd4ab1fc9 100644 --- a/js/packages/truapi-host/README.md +++ b/js/packages/truapi-host/README.md @@ -1,54 +1,98 @@ # @parity/truapi-host -WASM-backed TrUAPI host runtime. It embeds the `truapi-server` Rust core (compiled to WASM) -behind a Web Worker provider, plus per-environment integration entry points. It is the -counterpart to the native Android/iOS host shells. +WASM-backed TrUAPI host runtime. It embeds the `truapi-server` Rust core (compiled to WASM) behind a Web Worker +provider, plus per-environment integration entry points. It is the counterpart to the native Android/iOS host shells. ## Entry points The package exposes tree-shakeable subpath exports — import only what your environment needs: -| Import | Provides | -| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | -| `@parity/truapi-host` | Shared runtime types plus generated typed host callback contracts. | -| `@parity/truapi-host/web` | Browser pairing host: `createIframeHost` (iframe MessageChannel handshake) and `createWebWorkerPairingHostRuntime`. | -| `@parity/truapi-host/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/web` | The raw browser `wasm-bindgen` glue, if you need to instantiate the core yourself. | -| `@parity/truapi-host/testing` | `createMockHost`: the in-memory host seam a product is tested against. | -| `@parity/truapi-host/testing/playwright` | Playwright fixture that boots the test host and embeds the product in an iframe. | -| `@parity/truapi-host/testing/server` | Node server that serves the host page and its bundle. | -| `@parity/truapi-host/testing/client` | `createMockClient`: a product client over the mock with no iframe, for unit tests. | -| `@parity/truapi-host/testing/dev-accounts` | Named dev accounts derived from fixed BIP-39 entropy, which sign for real. | -| `@parity/truapi-host/testing/host-page` | The browser half the fixture drives, for a suite that boots its own page. | -| `@parity/truapi-host/wasm/testing` | The raw glue for the signing-enabled bundle the test host runs on. | - -`scripts/build-wasm.mjs` builds two WASM bundles, both `--no-default-features`. -`wasm/web` is the production browser host and excludes -`WasmSigningHostRuntime`; `wasm/testing` adds the Rust `wasm-signing-host` and -`test-host` features, which is what lets the test host hold keys and answer -resource allocation as granted without allocating anything. -`ProductRuntimeConfig` configures the pairing host and requires no network -suffix. The signing constructor's configuration requires -`runtimeConfig.networkSuffix` in addition: the bare TLD (`dot`, `paseo`, or -`testnet`) matching the People chain and the wallet's onboarding -configuration. - -`runtimeConfig.assetHub` is required by both configurations, pairing and -signing. It is the Asset Hub genesis hash, in the same shape as -`runtimeConfig.people` and `runtimeConfig.bulletin`. Product manifests are read -from the dotNS contracts deployed there, so it is what makes a -`trustedProducts` grant resolvable: without a usable value no manifest resolves, -so every cross-product grant not already cached is refused, and the refusal is -indistinguishable from the other product having granted nothing. A config -omitting it is rejected. +| Import | Provides | +| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | +| `@parity/truapi-host` | Shared runtime types plus generated typed host callback contracts. | +| `@parity/truapi-host/web` | Browser pairing and signing hosts: `createIframeHost`, `createWebWorkerPairingHostRuntime`, and `createWebWorkerSigningHostRuntime`. | +| `@parity/truapi-host/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/web` | The raw browser `wasm-bindgen` glue, if you need to instantiate the core yourself. | +| `@parity/truapi-host/testing` | `createMockHost`: the in-memory host seam a product is tested against. | +| `@parity/truapi-host/testing/playwright` | Playwright fixture that boots the test host and embeds the product in an iframe. | +| `@parity/truapi-host/testing/server` | Node server that serves the host page and its bundle. | +| `@parity/truapi-host/testing/client` | `createMockClient`: a product client over the mock with no iframe, for unit tests. | +| `@parity/truapi-host/testing/dev-accounts` | Named dev accounts derived from fixed BIP-39 entropy, which sign for real. | +| `@parity/truapi-host/testing/host-page` | The browser half the fixture drives, for a suite that boots its own page. | +| `@parity/truapi-host/wasm/testing` | The raw glue for the signing-enabled bundle the test host runs on. | + +`scripts/build-wasm.mjs` builds two WASM bundles, both `--no-default-features`. `wasm/web` is the production browser +host and excludes `WasmSigningHostRuntime`; `wasm/testing` adds the Rust `wasm-signing-host` and `test-host` features, +which is what lets the test host hold keys and answer resource allocation as granted without allocating anything. A real +browser wallet instead needs a web bundle built with `--no-default-features --features wasm-signing-host`, without +`test-host`. That enables native signing and wallet administration without the testing-only allocation shortcuts. +`ProductRuntimeConfig` configures the pairing host and requires no network suffix. The signing constructor's +configuration requires `runtimeConfig.networkSuffix` in addition: the bare TLD (`dot`, `paseo`, or `testnet`) matching +the People chain and the wallet's onboarding configuration. + +`runtimeConfig.assetHub` is required by both configurations, pairing and signing. It is the Asset Hub genesis hash, in +the same shape as `runtimeConfig.people` and `runtimeConfig.bulletin`. Product manifests are read from the dotNS +contracts deployed there, so it is what makes a `trustedProducts` grant resolvable: without a usable value no manifest +resolves, so every cross-product grant not already cached is refused, and the refusal is indistinguishable from the +other product having granted nothing. A config omitting it is rejected. + +When a product requests a Statement Store allowance, the signing runtime first uses `confirmUserAction` for host-owned +approval UI, then registers the product-scoped allowance account through the platform's People-chain connection. +Registration requires the activated wallet root to belong to an eligible personhood ring. A randomly generated browser +account can still sign, but it cannot spend another identity's personhood allowance; use a pairing host with the mobile +Account Holder when that identity remains on the phone. + +After activating a local signing session, call `refreshLocalIdentity()` to read the configured Asset Hub's dotNS +ownership and install verified username metadata. +`registerLocalLiteUsername(baseUsername, identityBackendBaseUrl, onProgress?)` authenticates to the identity backend +with native UID proofs, submits the registration with the real RFC-0004 X25519 identifier key and Asset Hub time, then +monitors chain ownership until it is confirmed or the activation is disconnected/replaced. Backend acceptance alone is +not registration success. Slow confirmation and transient chain-read failures do not resubmit the claim or require +repeated manual refreshes. + +The optional callback receives `LocalIdentityProgress` (exported from `@parity/truapi-host/web`): `checking`, +`authenticating`, `submitting`, `confirming`, or `retrying` with a chain-read `error`. Stages reflect actual work, not +elapsed-time estimates. `confirming` means the backend accepted the request, not that the username is owned yet; only +the resolved promise confirms ownership. A retry does not submit another registration. Observer exceptions do not +interrupt the operation, and settled or disposed requests receive no further progress. + +Both methods return `LocalIdentity` (exported from `@parity/truapi-host/web`): the canonical lowercase `0x`-prefixed +`identityAccountId` and an optional verified `liteUsername`. The backend must allow the worker's origin, or the host +must provide an approved same-origin proxy. Secret material stays in the signing runtime. Disconnecting or replacing the +local activation invalidates an in-flight identity operation; concurrent identity operations are rejected. + +### Read-only wallet allowance inspection + +After activating a local signing session, trusted shell code can call: + +```ts +const snapshot = await runtime.getWalletAllowanceSnapshot(["example.paseo"]); +``` + +The result is a `WalletAllowanceSnapshot` exported from `@parity/truapi-host/web`. It reports Statement Store publishing +slots, PGAS claim capacity and balances, and Bulletin claim capacity and storage quotas. This is a signing-host admin +API, not a product protocol method. A bare local identity can inspect it without registering or refreshing a username. +Inspection never allocates, renews, signs, or changes wallet storage. + +Pass at most 32 unique canonical product IDs from trusted shell state. An empty list still inspects wallet-wide slots +and claims. PGAS balances cover each requested product's `Index(0)` account; Bulletin quotas use its distinct storage +allowance account. Other products, derivations, spending and historical attribution are outside this snapshot. + +Each section is independently available or unavailable. Available values carry finalized chain provenance and chain +time, with exact integer amounts and runtime-defined limits. PGAS claims also carry `membershipObservation` for the +separate People membership source. Missing metadata stays unknown, and a nonmember's policy limit is not presented as +usable capacity. + +Activation changes, disconnect, disposal and worker failure invalidate pending reads. The worker API rejects after 30 +seconds rather than leaving the caller pending; native read work may finish later, but cannot populate a replaced +wallet. Shells must additionally fence their selected network/product context. ## Bundler requirements -The worker imports the WASM glue by a literal specifier, so every bundler -resolves it statically and emits `truapi_server.js` as a chunk. Whether the -`truapi_server_bg.wasm` payload comes with it depends on the bundler: emitting -it requires treating `new URL("truapi_server_bg.wasm", import.meta.url)` inside -the glue as an asset reference, and not all of them do. +The worker imports the WASM glue by a literal specifier, so every bundler resolves it statically and emits +`truapi_server.js` as a chunk. Whether the `truapi_server_bg.wasm` payload comes with it depends on the bundler: +emitting it requires treating `new URL("truapi_server_bg.wasm", import.meta.url)` inside the glue as an asset reference, +and not all of them do. | Bundler | Emits the `.wasm`? | Host action | | ------------------- | ------------------ | -------------------------------------------------------------- | @@ -58,33 +102,27 @@ the glue as an asset reference, and not all of them do. | esbuild | No | Copy manually (see below). | | Bun (`bun build`) | No | Copy manually (see below). | -esbuild and Bun pass `new URL(..., import.meta.url)` through verbatim: the build -succeeds and the glue chunk is emitted, but no `.wasm` is written and the worker -404s at runtime. No flag changes this — `--loader:.wasm=file` only fires on -`import` statements, never on `new URL`. Hosts on those bundlers must copy -`truapi_server_bg.wasm` out of `@parity/truapi-host/dist/wasm/web/` into the same -output directory as the emitted `truapi_server-*.js` chunk, since the glue -resolves the payload relative to its own URL. +esbuild and Bun pass `new URL(..., import.meta.url)` through verbatim: the build succeeds and the glue chunk is emitted, +but no `.wasm` is written and the worker 404s at runtime. No flag changes this — `--loader:.wasm=file` only fires on +`import` statements, never on `new URL`. Hosts on those bundlers must copy `truapi_server_bg.wasm` out of +`@parity/truapi-host/dist/wasm/web/` into the same output directory as the emitted `truapi_server-*.js` chunk, since the +glue resolves the payload relative to its own URL. -Running Vite under Bun (`bunx --bun vite build`) uses Vite's bundler and is -unaffected; only `bun build` is. +Running Vite under Bun (`bunx --bun vite build`) uses Vite's bundler and is unaffected; only `bun build` is. -The literal import makes the worker a code-split chunk, so a Vite host must ask -for ES workers; the default `iife` format cannot code-split and fails the build: +The literal import makes the worker a code-split chunk, so a Vite host must ask for ES workers; the default `iife` +format cannot code-split and fails the build: ```ts export default defineConfig({ worker: { format: "es" } }); ``` -Only the `.wasm` the glue references is emitted, and bundlers content-hash it — -Vite writes `assets/truapi_server_bg-.wasm`, webpack writes a bare -`.wasm`. The `.wasm.gz` / `.wasm.br` sidecars under `dist/wasm/web/` -therefore cannot be copied into a host's output: `gzip_static` / -`brotli_static` serve `.gz` / `.br`, and the request path now -carries the bundler's hash. Hosts that serve precompressed assets should -generate them from their own build output, after hashing — either a post-build -pass over `dist` (gzip level 9 and brotli max quality reproduce the sidecars -byte for byte) or a bundler plugin: +Only the `.wasm` the glue references is emitted, and bundlers content-hash it — Vite writes +`assets/truapi_server_bg-.wasm`, webpack writes a bare `.wasm`. The `.wasm.gz` / `.wasm.br` sidecars under +`dist/wasm/web/` therefore cannot be copied into a host's output: `gzip_static` / `brotli_static` serve +`.gz` / `.br`, and the request path now carries the bundler's hash. Hosts that serve precompressed assets +should generate them from their own build output, after hashing — either a post-build pass over `dist` (gzip level 9 and +brotli max quality reproduce the sidecars byte for byte) or a bundler plugin: ```ts import { compression } from "vite-plugin-compression2"; @@ -101,17 +139,15 @@ export default defineConfig({ }); ``` -webpack hosts get the same result from `compression-webpack-plugin`. Skipping -this ships the full 1.4 MB `.wasm` where about 600 kB (gzip) or 470 kB (brotli) -would do — and a server configured with `gzip_static` but no dynamic `gzip on` +webpack hosts get the same result from `compression-webpack-plugin`. Skipping this ships the full 1.4 MB `.wasm` where +about 600 kB (gzip) or 470 kB (brotli) would do — and a server configured with `gzip_static` but no dynamic `gzip on` has no fallback. ## Optional capabilities -`HostCallbacks` groups are required except those listed on the Rust -`OptionalPlatform` super-trait, which are emitted as optional members. Omit one -and the core answers its product calls with `Unsupported`; supply it and the -whole group must be implemented: +`HostCallbacks` groups are required except those listed on the Rust `OptionalPlatform` super-trait, which are emitted as +optional members. Omit one and the core answers its product calls with `Unsupported`; supply it and the whole group must +be implemented: ```ts const callbacks: HostCallbacks = { @@ -124,25 +160,21 @@ const callbacks: HostCallbacks = { }; ``` -`permissionStatus.devicePermissionStatus` must answer from the OS without -prompting. Supply it and the core revalidates a stored device grant against it -before answering the product, so a capability the OS has since revoked or reset -stops reading as usable. Omit it and a stored grant answers on its own. +`permissionStatus.devicePermissionStatus` must answer from the OS without prompting. Supply it and the core revalidates +a stored device grant against it before answering the product, so a capability the OS has since revoked or reset stops +reading as usable. Omit it and a stored grant answers on its own. -`pocket` serves the host's card collection. `subscribePocketCards` emits the -calling product's cards and every later replacement, and `removePocketCard` -takes one out. The host owns the collection: removing an absent card succeeds, -and a card the host pins is refused with `Privileged`. +`pocket` serves the host's card collection. `subscribePocketCards` emits the calling product's cards and every later +replacement, and `removePocketCard` takes one out. The host owns the collection: removing an absent card succeeds, and a +card the host pins is refused with `Privileged`. -Under `createWebWorkerPairingHostRuntime` the presence of each optional group is -reported to the worker in its `init` message, so the core sees the same -capability set on both sides of the boundary. +Under `createWebWorkerPairingHostRuntime` the presence of each optional group is reported to the worker in its `init` +message, so the core sees the same capability set on both sides of the boundary. ### Product-rendered bodies -A host can ask a product to draw one body — a chat message, an input-widget -candidate, a Pocket card — and send back what the user does with it. Both entry -points live on the product provider and are present only on runtimes holding a +A host can ask a product to draw one body — a chat message, an input-widget candidate, a Pocket card — and send back +what the user does with it. Both entry points live on the product provider and are present only on runtimes holding a live channel to the core: ```ts @@ -173,68 +205,53 @@ await provider.publishRendererAction!({ stop(); // stop rendering; safe to call more than once ``` -A `TextField` value change instead carries the UTF-8 bytes of the new value, -with no length prefix. - -`render` reports failure through `onError` rather than throwing, so one dead -render cannot take the surrounding surface with it. Exactly one terminal fires -per render: `onComplete` means the last tree delivered stands, `onError` means -it is partial and must not be shown as final. A product that declines the -render, a tree that fails to decode, a closed connection, and a throwing -renderer all arrive as `onError`. An open render holds one worker reference for -the provider's product, released when the stream ends or the disposer runs, so -the product's worker stays up for as long as something is being drawn. - -`publishChatAction` is the path for posted messages, commands and host-drawn -`Actions` buttons. Each action entry point sits behind its own service's access -policy: the renderer refuses a connection that is not a `Worker` execution, and +A `TextField` value change instead carries the UTF-8 bytes of the new value, with no length prefix. + +`render` reports failure through `onError` rather than throwing, so one dead render cannot take the surrounding surface +with it. Exactly one terminal fires per render: `onComplete` means the last tree delivered stands, `onError` means it is +partial and must not be shown as final. A product that declines the render, a tree that fails to decode, a closed +connection, and a throwing renderer all arrive as `onError`. An open render holds one worker reference for the +provider's product, released when the stream ends or the disposer runs, so the product's worker stays up for as long as +something is being drawn. + +`publishChatAction` is the path for posted messages, commands and host-drawn `Actions` buttons. Each action entry point +sits behind its own service's access policy: the renderer refuses a connection that is not a `Worker` execution, and chat additionally requires a live session. -Two rules the core cannot check are the host's to keep: send a render context -only for a surface the product's manifest `includes`, and publish a renderer -action only from the current tree of an open render stream. +Two rules the core cannot check are the host's to keep: send a render context only for a surface the product's manifest +`includes`, and publish a renderer action only from the current tree of an open render stream. ## Product account addresses -A host that stores the core's `ProductSubtree` slot can name the account a -review will sign with, so the review can carry an address and a fee rather than -a bare derivation path. Both calls are pure and need no runtime or session, so +A host that stores the core's `ProductSubtree` slot can name the account a review will sign with, so the review can +carry an address and a fee rather than a bare derivation path. Both calls are pure and need no runtime or session, so `default()` alone is enough: ```ts -import init, { - deriveProductAccountPublicKey, - productAccountAddress, -} from "@parity/truapi-host/wasm/web"; +import init, { deriveProductAccountPublicKey, productAccountAddress } from "@parity/truapi-host/wasm/web"; import { DerivationIndex } from "@parity/truapi"; await init(); // `subtreePublicKey` is the 32 bytes read from the host's own // `ProductSubtree { sessionId, productId }` slot. -const publicKey = deriveProductAccountPublicKey( - subtreePublicKey, - DerivationIndex.enc(account.derivationIndex), -); +const publicKey = deriveProductAccountPublicKey(subtreePublicKey, DerivationIndex.enc(account.derivationIndex)); const address = productAccountAddress(publicKey); ``` -The index crosses as a SCALE-encoded `DerivationIndex`, the same value a review -already carries, so the 32-byte chain code behind it stays core-owned and a host -never reconstructs it. `productAccountAddress` applies the prefix host-spec C.6 -fixes, rather than leaving each host to choose one. +The index crosses as a SCALE-encoded `DerivationIndex`, the same value a review already carries, so the 32-byte chain +code behind it stays core-owned and a host never reconstructs it. `productAccountAddress` applies the prefix host-spec +C.6 fixes, rather than leaving each host to choose one. ## Generated WASM artefacts -The ignored bundle under `dist/wasm/web/` is built with host-owned chain access. -Hosts wire their JSON-RPC provider through `chainConnect`; if they omit it, -chain calls fail with the core's standard unavailable error. Release builds use -the workspace size-optimized Rust profile plus `wasm-opt -Oz`, validate that -debug/name/producers custom sections were stripped, and emit `.wasm.gz` and -`.wasm.br` sidecars for hosts that serve precompressed assets. +The ignored bundle under `dist/wasm/web/` is built with host-owned chain access. Hosts wire their JSON-RPC provider +through `chainConnect`; if they omit it, chain calls fail with the core's standard unavailable error. Release builds use +the workspace size-optimized Rust profile plus `wasm-opt -Oz`, validate that debug/name/producers custom sections were +stripped, and emit `.wasm.gz` and `.wasm.br` sidecars for hosts that serve precompressed assets. -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): +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 @@ -246,13 +263,9 @@ npm run build:wasm # or `make wasm` from the repo root import HostWorker from "@parity/truapi-host/worker-runtime?worker"; import { createWebWorkerPairingHostRuntime } from "@parity/truapi-host/web"; -const runtime = await createWebWorkerPairingHostRuntime( - new HostWorker(), - callbacks, - { - hostConfig, - }, -); +const runtime = await createWebWorkerPairingHostRuntime(new HostWorker(), callbacks, { + hostConfig, +}); const firstProvider = await runtime.createProvider({ productId: "first.dot" }); const secondProvider = await runtime.createProvider({ @@ -260,17 +273,15 @@ const secondProvider = await runtime.createProvider({ }); ``` -`@parity/truapi-host/web` also exports `createIframeHost` for the -protocol-iframe MessageChannel handshake. Host code creates one worker runtime -and then opens one provider per product id. +`@parity/truapi-host/web` also exports `createIframeHost` for the protocol-iframe MessageChannel handshake. Host code +creates one worker runtime and then opens one provider per product id. ## Session lifecycle -The core owns the session; the host owns persistence. At boot the core restores -the `AuthSession` slot on its own and reports the outcome through the `auth` -callback, `Disconnected` included, so a host waits for the first -`authStateChanged` instead of treating silence as "signed out". Every -transition below reports the resulting `AuthState` the same way. +The core owns the session; the host owns persistence. At boot the core restores the `AuthSession` slot on its own and +reports the outcome through the `auth` callback, `Disconnected` included, so a host waits for the first +`authStateChanged` instead of treating silence as "signed out". Every transition below reports the resulting `AuthState` +the same way. | Runtime method | Use it to | | ------------------------------- | ---------------------------------------------------------------------------- | @@ -283,11 +294,7 @@ transition below reports the resulting `AuthState` the same way. The boot order is create the runtime, restore, then open providers: ```ts -const runtime = await createWebWorkerPairingHostRuntime( - new HostWorker(), - callbacks, - { hostConfig }, -); +const runtime = await createWebWorkerPairingHostRuntime(new HostWorker(), callbacks, { hostConfig }); // Resolves once product frames may use the restored session; rejects when // there was nothing to restore. @@ -298,28 +305,25 @@ const provider = await runtime.createProvider({ productId: "first.dot" }); ### Statement-store traffic of the host's own -A host that runs its own statement-store traffic — P2P chat, device sync — signs -with the account its device advertises in the pairing QR. The paired wallet -registers that account's statement-store allowance while answering the -handshake, so it is the only account whose statements the network accepts from -this host. A locally minted key gets no allowance, and every submission fails -with `no allowance set for account`. +A host that runs its own statement-store traffic — P2P chat, device sync — signs with the account its device advertises +in the pairing QR. The paired wallet registers that account's statement-store allowance while answering the handshake, +so it is the only account whose statements the network accepts from this host. A locally minted key gets no allowance, +and every submission fails with `no allowance set for account`. | Value | Where | | ---------------------------------------- | -------------------------------------- | | `SessionUiInfo.deviceStatementAccountId` | On every `AuthState.Connected` | | `getDeviceStatementKey()` | Runtime method, 64-byte sr25519 secret | -Peers are told to address that same account, so it is also what a host derives -its own statement topics from. Both are `undefined` without an active session, -and the account is rotated per login: read it from the current session rather +Peers are told to address that same account, so it is also what a host derives its own statement topics from. Both are +`undefined` without an active session, and the account is rotated per login: read it from the current session rather than caching it across sign-ins. ## Worker lifecycle -A product has one worker, and the core keeps one reference count per worker. The host takes a reference while a -modality holder is on screen or in flight, such as a chat room the product serves, and releases it when the holder -leaves. The host runs and stops the worker executable itself; the runtime only tells it when demand crosses zero. +A product has one worker, and the core keeps one reference count per worker. The host takes a reference while a modality +holder is on screen or in flight, such as a chat room the product serves, and releases it when the holder leaves. The +host runs and stops the worker executable itself; the runtime only tells it when demand crosses zero. | Runtime method | Use it to | | --------------------------------- | ---------------------------------------------------------------- | @@ -342,52 +346,43 @@ once, after the last. A `wanted: false` is permission to stop, not an order: a h ## Debugging (dev-only) -The worker can stream every product↔core wire frame to the wire debugger. It is -off by default and enabled purely from the host page — the product needs no -changes. Two conditions must **both** hold or nothing dials and the core installs +The worker can stream every product↔core wire frame to the wire debugger. It is off by default and enabled purely from +the host page — the product needs no changes. Two conditions must **both** hold or nothing dials and the core installs no tap: -1. **The host page is a dev build.** The dial sits behind a hard - `import.meta.env.DEV` gate, which bundlers replace with a boolean literal: in - a production bundle it returns `null` unconditionally, so no stored key can - turn the tap on. A production build that shows no frames is this gate, not a - broken debugger — and it says so: with the key set but the gate closed, the - host logs once that the dial is compiled out, rather than staying silent and - reading as a broken tool. That matters for a host whose only local build is - production-mode; `NODE_ENV=development` is what opens the gate under Vite. -2. **The host origin's `localStorage` carries a `ws://` loopback URL**, read on - the host page at runtime boot and forwarded to the worker in its `init` - message: +1. **The host page is a dev build.** The dial sits behind a hard `import.meta.env.DEV` gate, which bundlers replace with + a boolean literal: in a production bundle it returns `null` unconditionally, so no stored key can turn the tap on. A + production build that shows no frames is this gate, not a broken debugger — and it says so: with the key set but the + gate closed, the host logs once that the dial is compiled out, rather than staying silent and reading as a broken + tool. That matters for a host whose only local build is production-mode; `NODE_ENV=development` is what opens the + gate under Vite. +2. **The host origin's `localStorage` carries a `ws://` loopback URL**, read on the host page at runtime boot and + forwarded to the worker in its `init` message: ```js localStorage.setItem("truapi:debugger", "ws://127.0.0.1:9231"); ``` -Run the debugger at the other end (`@parity/truapi-debugger`, `npm run serve`, -`127.0.0.1:9231`). On the next runtime boot the worker dials that URL and (via -the Rust core's `DebugSink` tap) sends each frame as `{ channelId, dir, frame }`. +Run the debugger at the other end (`@parity/truapi-debugger`, `npm run serve`, `127.0.0.1:9231`). On the next runtime +boot the worker dials that URL and (via the Rust core's `DebugSink` tap) sends each frame as +`{ channelId, dir, frame }`. -The URL must be `ws://` on a loopback host. Anything else — `wss://`, `http://`, -a LAN or public address, a non-loopback hostname — yields an inert link and a -`wire debugger URL rejected` console warning; there is no certificate or `wss` -path. Prefer the literal `127.0.0.1` over `localhost`: `localhost` passes the -gate, but it resolves `::1` first on macOS while the debugger binds `127.0.0.1` -alone, so the same URL handed to a native host (`truapi-server`'s `WsDebugSink` +The URL must be `ws://` on a loopback host. Anything else — `wss://`, `http://`, a LAN or public address, a non-loopback +hostname — yields an inert link and a `wire debugger URL rejected` console warning; there is no certificate or `wss` +path. Prefer the literal `127.0.0.1` over `localhost`: `localhost` passes the gate, but it resolves `::1` first on macOS +while the debugger binds `127.0.0.1` alone, so the same URL handed to a native host (`truapi-server`'s `WsDebugSink` dials the first resolved address) silently never connects. -The debugger owns all decoding and decodes every frame it can, including signing -and payment payloads; its safety is the dev-build gate above, not redaction. See -`js/packages/truapi-debugger/README.md` for the tap, the envelope, and the +The debugger owns all decoding and decodes every frame it can, including signing and payment payloads; its safety is the +dev-build gate above, not redaction. See `js/packages/truapi-debugger/README.md` for the tap, the envelope, and the host-dials-debugger topology. ## Publishing -This package is published by the root `Release` workflow through -`paritytech/npm_publish_automation`. Do not run `npm publish` locally. Cut a -`release:` PR with a changeset for `@parity/truapi-host`; the workflow builds -the generated host bindings, the browser WASM bundle, packs the tarball, and -publishes it when the `@parity/truapi-host@` tag does not already -exist. +This package is published by the root `Release` workflow through `paritytech/npm_publish_automation`. Do not run +`npm publish` locally. Cut a `release:` PR with a changeset for `@parity/truapi-host`; the workflow builds the generated +host bindings, the browser WASM bundle, packs the tarball, and publishes it when the `@parity/truapi-host@` tag +does not already exist. ## Architecture diff --git a/js/packages/truapi-host/src/testing/host-page.ts b/js/packages/truapi-host/src/testing/host-page.ts index 20c83eb32..0453ce576 100644 --- a/js/packages/truapi-host/src/testing/host-page.ts +++ b/js/packages/truapi-host/src/testing/host-page.ts @@ -24,7 +24,7 @@ // debug when a suite fails. import { createIframeHost } from "../web/create-iframe-host.js"; -import { createWebWorkerPairingHostRuntime } from "../web/create-worker-host-runtime.js"; +import { createWebWorkerSigningHostRuntime } from "../web/create-worker-host-runtime.js"; import { createMockHost, mockRuntimeConfig, @@ -215,7 +215,7 @@ export async function startTestHost( worker = new Worker(options.workerUrl ?? "/test-host-worker.js", { type: "module", }); - workerRuntime = (await createWebWorkerPairingHostRuntime( + workerRuntime = (await createWebWorkerSigningHostRuntime( worker, host.callbacks, { hostConfig: hostConfig as never, role: "signing" }, diff --git a/js/packages/truapi-host/src/wallet-allowances.test.ts b/js/packages/truapi-host/src/wallet-allowances.test.ts new file mode 100644 index 000000000..a74802eab --- /dev/null +++ b/js/packages/truapi-host/src/wallet-allowances.test.ts @@ -0,0 +1,194 @@ +import { describe, expect, it } from "bun:test"; +import { + validateWalletAllowanceSnapshot, + type WalletAllowanceSnapshot, +} from "./wallet-allowances.js"; + +const account = `0x${"11".repeat(32)}`; +const observation = { + genesisHash: `0x${"22".repeat(32)}`, + blockHash: `0x${"33".repeat(32)}`, + blockNumber: 42, + specVersion: 1, + chainTimestamp: 1_800_000_000, +}; +function snapshot(): WalletAllowanceSnapshot { + return { + schemaVersion: 1, + identityAccountId: account, + networkSuffix: "paseo", + productIds: ["chat.dot"], + statementStore: { + status: "available", + observation, + value: { + period: 4, + resetsAt: 1_800_100_000, + graceSeconds: 30, + replacementCooldownSeconds: 60, + pools: [ + { + collection: "LitePeople", + membership: "verified", + selected: true, + limit: 2, + used: 1, + remaining: 1, + slots: [{ index: 0, accountId: account }], + }, + ], + }, + }, + pgasClaims: { status: "unavailable", reason: "chain unavailable" }, + pgasBalances: { + status: "available", + observation, + value: { + assetId: "1984", + decimals: null, + symbol: null, + accounts: [ + { + productId: "chat.dot", + accountId: account, + derivationIndex: 0, + balance: "9007199254740993123456", + }, + ], + }, + }, + bulletinClaims: { status: "unavailable", reason: "chain unavailable" }, + bulletinQuotas: { status: "unavailable", reason: "chain unavailable" }, + }; +} + +describe("native wallet allowance snapshot validation", () => { + it("rejects malformed capacity instead of exposing a fabricated free slot", () => { + const value = snapshot(); + if (value.statementStore.status !== "available") throw new Error("fixture"); + value.statementStore.value.pools[0]!.remaining = 2; + expect(() => + validateWalletAllowanceSnapshot(value, account, "paseo", ["chat.dot"]), + ).toThrow(); + }); + + it("never treats a nonmember policy limit as usable capacity", () => { + const value = snapshot(); + if (value.statementStore.status !== "available") throw new Error("fixture"); + const pool = value.statementStore.value.pools[0]!; + pool.membership = "not-found"; + pool.selected = false; + pool.remaining = 0; + validateWalletAllowanceSnapshot(value, account, "paseo", ["chat.dot"]); + pool.remaining = pool.limit - pool.used; + expect(() => + validateWalletAllowanceSnapshot(value, account, "paseo", ["chat.dot"]), + ).toThrow(); + pool.remaining = 0; + pool.selected = true; + expect(() => + validateWalletAllowanceSnapshot(value, account, "paseo", ["chat.dot"]), + ).toThrow(); + }); + + it("rejects imprecise numbers and missing chain provenance", () => { + const value = snapshot(); + if (value.statementStore.status !== "available") throw new Error("fixture"); + value.statementStore.observation = { + ...observation, + blockNumber: Number.MAX_SAFE_INTEGER + 1, + }; + expect(() => + validateWalletAllowanceSnapshot(value, account, "paseo", ["chat.dot"]), + ).toThrow(); + value.statementStore.observation = { ...observation, blockHash: "" }; + expect(() => + validateWalletAllowanceSnapshot(value, account, "paseo", ["chat.dot"]), + ).toThrow(); + }); + + it("requires separate People provenance for Asset Hub claim eligibility", () => { + const value = snapshot(); + value.pgasClaims = { + status: "available", + observation, + value: { + period: 4, + resetsAt: 1_800_100_000, + pools: [], + assetId: "1984", + claimAmount: "100", + membershipObservation: { ...observation, genesisHash: account }, + }, + }; + validateWalletAllowanceSnapshot(value, account, "paseo", ["chat.dot"]); + value.pgasClaims.value.membershipObservation.blockHash = ""; + expect(() => + validateWalletAllowanceSnapshot(value, account, "paseo", ["chat.dot"]), + ).toThrow(); + }); + + it("rejects partial account batches instead of treating omissions as zero", () => { + const value = snapshot(); + if (value.pgasBalances.status !== "available") throw new Error("fixture"); + value.pgasBalances.value.accounts = []; + expect(() => + validateWalletAllowanceSnapshot(value, account, "paseo", ["chat.dot"]), + ).toThrow(); + }); + + it("rejects lossy numeric balances and unexplained null balances", () => { + const value = snapshot(); + validateWalletAllowanceSnapshot(value, account, "paseo", ["chat.dot"]); + if (value.pgasBalances.status !== "available") throw new Error("fixture"); + const numericBalance = { + ...value, + pgasBalances: { + ...value.pgasBalances, + value: { + ...value.pgasBalances.value, + accounts: [ + { + ...value.pgasBalances.value.accounts[0], + balance: 9007199254740992, + }, + ], + }, + }, + }; + expect(() => + validateWalletAllowanceSnapshot(numericBalance, account, "paseo", [ + "chat.dot", + ]), + ).toThrow(); + value.pgasBalances.value.accounts[0]!.balance = null; + expect(() => + validateWalletAllowanceSnapshot(value, account, "paseo", ["chat.dot"]), + ).toThrow(); + value.pgasBalances.value.accounts[0]!.error = "RPC unavailable"; + validateWalletAllowanceSnapshot(value, account, "paseo", ["chat.dot"]); + }); + + it("rejects snapshots from another wallet, network, schema or product scope", () => { + const value = snapshot(); + expect(() => + validateWalletAllowanceSnapshot(value, `0x${"44".repeat(32)}`, "paseo", [ + "chat.dot", + ]), + ).toThrow(); + expect(() => + validateWalletAllowanceSnapshot(value, account, "dot", ["chat.dot"]), + ).toThrow(); + expect(() => + validateWalletAllowanceSnapshot(value, account, "paseo", ["other.dot"]), + ).toThrow(); + expect(() => + validateWalletAllowanceSnapshot( + { ...value, schemaVersion: 2 }, + account, + "paseo", + ["chat.dot"], + ), + ).toThrow(); + }); +}); diff --git a/js/packages/truapi-host/src/wallet-allowances.ts b/js/packages/truapi-host/src/wallet-allowances.ts new file mode 100644 index 000000000..f29cb8294 --- /dev/null +++ b/js/packages/truapi-host/src/wallet-allowances.ts @@ -0,0 +1,285 @@ +export type AllowanceCollection = "People" | "LitePeople"; +export interface AllowanceObservation { + genesisHash: string; + blockHash: string; + blockNumber: number; + specVersion: number; + /** Unix seconds from the pinned block. */ + chainTimestamp: number; +} +export type AllowanceSection = + | { status: "available"; observation: AllowanceObservation; value: T } + | { status: "unavailable"; reason: string }; +export interface AllowanceSlot { + index: number; + accountId?: string; + productId?: string; + label?: string; + since?: number; +} +export interface AllowancePool { + collection: AllowanceCollection; + membership: "verified" | "not-found"; + selected: boolean; + limit: number; + used: number; + remaining: number; + slots: AllowanceSlot[]; +} +export interface AllowanceClaims { + period: number; + resetsAt: number; + pools: AllowancePool[]; +} +export interface StatementAllowanceSnapshot extends AllowanceClaims { + graceSeconds: number; + replacementCooldownSeconds: number; +} +export interface PgasClaimsSnapshot extends AllowanceClaims { + /** Membership is observed on People, separately from Asset Hub claim storage. */ + membershipObservation: AllowanceObservation; + assetId: string; + /** Integer base units, never a floating-point amount. */ + claimAmount: string; +} +export interface PgasBalancesSnapshot { + assetId: string; + decimals: number | null; + symbol: string | null; + accounts: Array<{ + productId: string; + accountId: string; + derivationIndex: 0; + /** Total balance, not a spendability promise. */ + balance: string | null; + error?: string; + }>; +} +export interface BulletinQuota { + productId: string; + accountId: string; + status: "active" | "expired" | "missing" | "unavailable"; + bytesUsed?: string; + bytesLimit?: string; + bytesRemaining?: string; + transactionsUsed?: number; + transactionsLimit?: number; + transactionsRemaining?: number; + expiresAtBlock?: number; + error?: string; +} +export interface WalletAllowanceSnapshot { + schemaVersion: 1; + identityAccountId: string; + networkSuffix: string; + productIds: string[]; + statementStore: AllowanceSection; + pgasClaims: AllowanceSection; + pgasBalances: AllowanceSection; + bulletinClaims: AllowanceSection; + bulletinQuotas: AllowanceSection<{ accounts: BulletinQuota[] }>; +} + +/** Bound trusted shell hints before crossing the worker boundary. Native code + * applies the canonical product-ID rules before deriving or querying accounts. */ +export function validateAllowanceProductIds(value: unknown): string[] { + if ( + !Array.isArray(value) || + value.length > 32 || + new Set(value).size !== value.length + ) { + throw new Error( + "allowance inspection requires at most 32 unique nonempty product IDs", + ); + } + for (const id of value) { + if (typeof id !== "string" || id.trim().length === 0) { + throw new Error("allowance inspection requires nonempty product IDs"); + } + } + return [...value]; +} + +function requireValid(condition: unknown): asserts condition { + if (!condition) throw new Error("invalid wallet allowance snapshot"); +} +function record(value: unknown): Record { + requireValid( + typeof value === "object" && value !== null && !Array.isArray(value), + ); + return value as Record; +} +function text(value: unknown): value is string { + return typeof value === "string" && value.length > 0; +} +function uint(value: unknown): value is number { + return typeof value === "number" && Number.isSafeInteger(value) && value >= 0; +} +function units(value: unknown): value is string { + return typeof value === "string" && /^(0|[1-9][0-9]*)$/.test(value); +} +function hash(value: unknown): value is string { + return typeof value === "string" && /^0x[0-9a-f]{64}$/.test(value); +} +function array(value: unknown): unknown[] { + requireValid(Array.isArray(value)); + return value; +} +function claims(value: Record): void { + requireValid(uint(value.period) && uint(value.resetsAt)); + const collections = new Set(); + for (const item of array(value.pools)) { + const pool = record(item); + requireValid( + (pool.collection === "People" || pool.collection === "LitePeople") && + !collections.has(pool.collection), + ); + collections.add(pool.collection); + requireValid( + (pool.membership === "verified" || pool.membership === "not-found") && + typeof pool.selected === "boolean" && + uint(pool.limit) && + uint(pool.used) && + uint(pool.remaining) && + (pool.membership === "not-found" + ? pool.remaining === 0 && pool.selected === false + : pool.remaining === Math.max(0, pool.limit - pool.used)), + ); + const indices = new Set(); + for (const entry of array(pool.slots)) { + const slot = record(entry); + requireValid( + uint(slot.index) && + !indices.has(slot.index) && + (slot.accountId === undefined || hash(slot.accountId)) && + (slot.productId === undefined || text(slot.productId)) && + (slot.label === undefined || text(slot.label)) && + (slot.since === undefined || uint(slot.since)), + ); + indices.add(slot.index); + } + } +} +function observation(value: unknown): void { + const item = record(value); + requireValid( + hash(item.genesisHash) && + hash(item.blockHash) && + uint(item.blockNumber) && + uint(item.specVersion) && + uint(item.chainTimestamp), + ); +} +function section( + value: unknown, + validate: (value: Record) => void, +): void { + const item = record(value); + if (item.status === "unavailable") { + requireValid(text(item.reason)); + return; + } + requireValid(item.status === "available"); + observation(item.observation); + validate(record(item.value)); +} + +/** Validate once at the native JSON boundary, before treating an observation as + * current wallet data. Malformed data is rejected, never converted to capacity. */ +export function validateWalletAllowanceSnapshot( + value: unknown, + identityAccountId: string, + networkSuffix: string, + productIds: readonly string[], +): asserts value is WalletAllowanceSnapshot { + const snapshot = record(value); + requireValid( + snapshot.schemaVersion === 1 && + hash(snapshot.identityAccountId) && + snapshot.identityAccountId === identityAccountId && + snapshot.networkSuffix === networkSuffix, + ); + const requested = new Set(productIds); + const actual = validateAllowanceProductIds(snapshot.productIds); + requireValid( + actual.length === requested.size && actual.every((id) => requested.has(id)), + ); + const accounts = ( + value: unknown, + validate: (account: Record) => void, + ) => { + const seen = new Set(); + for (const entry of array(value)) { + const account = record(entry); + requireValid( + text(account.productId) && + requested.has(account.productId) && + !seen.has(account.productId) && + hash(account.accountId), + ); + seen.add(account.productId); + validate(account); + } + requireValid(seen.size === requested.size); + }; + section(snapshot.statementStore, (value) => { + claims(value); + requireValid( + uint(value.graceSeconds) && uint(value.replacementCooldownSeconds), + ); + }); + section(snapshot.pgasClaims, (value) => { + claims(value); + observation(value.membershipObservation); + requireValid(text(value.assetId) && units(value.claimAmount)); + }); + section(snapshot.pgasBalances, (value) => { + requireValid( + text(value.assetId) && + (value.decimals === null || uint(value.decimals)) && + (value.symbol === null || text(value.symbol)), + ); + accounts(value.accounts, (account) => { + requireValid( + account.derivationIndex === 0 && + (units(account.balance) || + (account.balance === null && text(account.error))) && + (account.error === undefined || text(account.error)), + ); + }); + }); + section(snapshot.bulletinClaims, claims); + section(snapshot.bulletinQuotas, (value) => + accounts(value.accounts, (account) => { + requireValid( + ["active", "expired", "missing", "unavailable"].includes( + String(account.status), + ), + ); + for (const key of ["bytesUsed", "bytesLimit", "bytesRemaining"]) { + requireValid(account[key] === undefined || units(account[key])); + } + for (const key of [ + "transactionsUsed", + "transactionsLimit", + "transactionsRemaining", + "expiresAtBlock", + ]) { + requireValid(account[key] === undefined || uint(account[key])); + } + requireValid(account.error === undefined || text(account.error)); + if (account.status === "unavailable") requireValid(text(account.error)); + if (account.status === "active" || account.status === "expired") { + requireValid( + units(account.bytesUsed) && + units(account.bytesLimit) && + units(account.bytesRemaining) && + uint(account.transactionsUsed) && + uint(account.transactionsLimit) && + uint(account.transactionsRemaining) && + uint(account.expiresAtBlock), + ); + } + }), + ); +} diff --git a/js/packages/truapi-host/src/wasm-module.ts b/js/packages/truapi-host/src/wasm-module.ts index 66562f2f0..5bffd253e 100644 --- a/js/packages/truapi-host/src/wasm-module.ts +++ b/js/packages/truapi-host/src/wasm-module.ts @@ -4,6 +4,8 @@ // these interfaces so the worker can name it in a statically analysable import. import type { PermissionAuthorizationRuntime } from "./worker-permission-authorization.js"; +import type { LocalIdentity } from "./worker-protocol.js"; +import type { WalletAllowanceSnapshot } from "./wallet-allowances.js"; /** Cancellable handle on one live render stream inside the core. */ export interface WorkerRendererSubscription { @@ -43,15 +45,13 @@ export interface WorkerProductRuntime { /** What the host does with a product's worker after demand on it changed. */ export type WorkerTransition = "Start" | "Stop"; -/** The long-lived pairing-host runtime product cores are created from. */ -export interface WorkerPairingHostRuntime extends PermissionAuthorizationRuntime { +/** Runtime operations shared by paired and browser-local signing hosts. */ +export interface WorkerHostRuntime extends PermissionAuthorizationRuntime { productRuntime( product: unknown, coreCallbacks: unknown, ): WorkerProductRuntime; disconnectSession(): Promise; - cancelPairing(): void; - notifySessionStoreChanged(): void; sessionChatIdentityKey(): Uint8Array | undefined; deviceStatementKey(): Uint8Array | undefined; deviceEncryptionKey(): Promise; @@ -59,9 +59,7 @@ export interface WorkerPairingHostRuntime extends PermissionAuthorizationRuntime productId: string, timeoutMs?: number, ): Promise; - activateStoredSession(): Promise; - activateExternalSession(blob: Uint8Array): Promise; - resetSessionState(): Promise; + clearProductState(productId: string): Promise; /** * Take one reference on the product's worker. The first one reports * `"Start"` through the runtime's `workerDemandChanged` callback. @@ -74,26 +72,50 @@ export interface WorkerPairingHostRuntime extends PermissionAuthorizationRuntime free(): void; } +/** The long-lived pairing-host runtime product cores are created from. */ +export interface WorkerPairingHostRuntime extends WorkerHostRuntime { + cancelPairing(): void; + notifySessionStoreChanged(): void; + activateStoredSession(): Promise; + activateExternalSession(blob: Uint8Array): Promise; + resetSessionState(): Promise; +} + /** - * The signing-host runtime, present only in the `testing` WASM bundle. + * A browser-local signing host activated from caller-owned entropy. * * A signing host owns the user's keys and establishes sessions from local - * entropy rather than by pairing with a wallet. The production `web` bundle is - * built without it on purpose, so this is optional on the module surface. + * entropy rather than by pairing with a wallet. It is present only in a core + * built with `wasm-signing-host`; the production `web` bundle is built without + * it, which is why the constructor is optional on {@link WasmModuleShape}. */ -export interface WorkerSigningHostRuntime extends WorkerPairingHostRuntime { +export interface WorkerSigningHostRuntime extends WorkerHostRuntime { activateLocalSession(secret: Uint8Array): Promise; /** * Activate and give the session a display name, which is what - * `account.get_user_id` answers with. Optional: a core built before this - * entry point existed exposes only {@link activateLocalSession}. + * `account.get_user_id` answers with. */ - /** Only on a core built with `wasm-signing-host`. */ - setGrantAllowancesUnchecked?(granted: boolean): void; - activateLocalSessionWithIdentity?( + activateLocalSessionWithIdentity( secret: Uint8Array, - liteUsername?: string | null, + liteUsername?: string, ): Promise; + /** Only on a core built with `wasm-signing-host`. */ + setGrantAllowancesUnchecked?(granted: boolean): void; + localIdentityContext(): { activationId: string; identityAccountId: string }; + localIdentityAuthProof( + activationId: string, + challenge: Uint8Array, + ): Uint8Array; + localLiteRegistrationBody( + activationId: string, + usernameBase: string, + verifier: Uint8Array, + ): Promise; + refreshLocalIdentity(activationId: string): Promise; + getWalletAllowanceSnapshot( + activationId: string, + productIds: string[], + ): Promise; } /** Module surface the wasm-pack glue exports. */ diff --git a/js/packages/truapi-host/src/wasm/web/truapi_server.d.ts b/js/packages/truapi-host/src/wasm/web/truapi_server.d.ts index 3a55fad2e..249b2b6f9 100644 --- a/js/packages/truapi-host/src/wasm/web/truapi_server.d.ts +++ b/js/packages/truapi-host/src/wasm/web/truapi_server.d.ts @@ -11,6 +11,7 @@ declare const init: WasmModuleShape["default"]; export default init; export const WasmPairingHostRuntime: WasmModuleShape["WasmPairingHostRuntime"]; +export const WasmSigningHostRuntime: WasmModuleShape["WasmSigningHostRuntime"]; export const WasmProductRuntime: WasmModuleShape["WasmProductRuntime"]; export const setLogLevel: (level: string) => void; export const deriveProductAccountPublicKey: WasmModuleShape["deriveProductAccountPublicKey"]; diff --git a/js/packages/truapi-host/src/web/create-worker-host-runtime.ts b/js/packages/truapi-host/src/web/create-worker-host-runtime.ts index ebd52eeb6..6c11d8c84 100644 --- a/js/packages/truapi-host/src/web/create-worker-host-runtime.ts +++ b/js/packages/truapi-host/src/web/create-worker-host-runtime.ts @@ -32,6 +32,8 @@ import type { RawCallbacks } from "../generated/host-callbacks-adapter.js"; import type { CallbackName, HostRole, + LocalIdentity, + LocalIdentityProgress, MainToWorker, SubscriptionName, WorkerToMain, @@ -39,11 +41,19 @@ import type { import { bytesToHex } from "@parity/truapi/scale"; import { startRawSubscription } from "../generated/worker-callbacks.js"; import { errorMessage, toError } from "../error.js"; +import { + validateAllowanceProductIds, + type WalletAllowanceSnapshot, +} from "../wallet-allowances.js"; export type WebWorkerHostConfig = Omit< ProductRuntimeConfig, "productId" | "executionKind" >; +export type WebWorkerSigningHostConfig = WebWorkerHostConfig & { + /** Bare dotNS network suffix (`dot`, `paseo`, or `testnet`). */ + networkSuffix: string; +}; export interface WorkerPairingHostRuntime { /** @@ -82,7 +92,10 @@ export interface WorkerPairingHostRuntime { * Signing hosts only. A pairing host has no local secret and rejects this: * it waits for a wallet to answer over the statement-store channel instead. */ - activateLocalSession(secret: Uint8Array, liteUsername?: string): Promise; + activateLocalSession( + secret: Uint8Array, + liteUsername?: string, + ): Promise; setGrantAllowancesUnchecked(granted: boolean): Promise; /** * Drop the active paired session without notifying the peer. Rejects on a @@ -133,6 +146,32 @@ export interface WorkerPairingHostRuntime { setLogLevel(level: LogLevel): void; dispose(): void; } +export interface WorkerSigningHostRuntime extends Omit< + WorkerPairingHostRuntime, + | "cancelPairing" + | "notifySessionStoreChanged" + | "activateStoredSession" + | "activateExternalSession" + | "resetSessionState" +> { + activateLocalSession(secret: Uint8Array): Promise; + activateLocalSessionWithIdentity( + secret: Uint8Array, + liteUsername?: string, + ): Promise; + /** Read dotNS ownership and install verified metadata into the native session. */ + refreshLocalIdentity(): Promise; + /** Read-only wallet-wide inspection bound to the native local activation. */ + getWalletAllowanceSnapshot( + productIds: string[], + ): Promise; + /** Complete native UID auth/proofs and wait for on-chain ownership confirmation. */ + registerLocalLiteUsername( + baseUsername: string, + identityBackendBaseUrl: string, + onProgress?: (progress: LocalIdentityProgress) => void, + ): Promise; +} interface CoreState { coreId: number; @@ -156,6 +195,11 @@ interface RenderEntry { interface RuntimeState { worker: Worker; + role: HostRole; + networkSuffix: string | undefined; + identityAccountId: string | null; + identityGeneration: number; + pendingAllowanceSnapshots: Map>; rawCallbacks: RawCallbacks; cores: Map; pendingCores: Map< @@ -194,6 +238,12 @@ interface RuntimeState { number, { resolve: () => void; reject: (error: Error) => void } >; + pendingLocalIdentities: Map< + number, + PendingEntry & { + onProgress?: (progress: LocalIdentityProgress) => void; + } + >; pendingPermissionAuthorizationStatuses: Map< number, { @@ -265,6 +315,8 @@ let nextDeviceStatementKeyRequestId = 0; let nextDeviceEncryptionKeyRequestId = 0; let nextProductSubtreePublicKeyRequestId = 0; let nextSessionActivationRequestId = 0; +let nextLocalIdentityRequestId = 0; +let nextAllowanceSnapshotRequestId = 0; let nextActionRequestId = 0; let nextRenderId = 0; @@ -434,6 +486,12 @@ function operationIdFrom(value: unknown): number | null { } } +/** + * Key one pending-operation hold. `OperationId` is unique per product, not per + * worker, so the product a `beginOperation`/`endOperation` arrived for has to be + * part of the key. Returns null if the encoded product will not decode, which + * drops the hold rather than letting it pin the worker forever. + */ function operationHold(encodedProduct: unknown, id: number): string | null { if (!(encodedProduct instanceof Uint8Array)) return null; try { @@ -784,6 +842,8 @@ function handleDeviceEncryptionKeyResponse( function rejectPendingRuntimeRequests(state: RuntimeState, error: Error): void { rejectAll(state.pendingDisconnects, error); rejectAll(state.pendingSessionActivations, error); + rejectAll(state.pendingLocalIdentities, error); + rejectAll(state.pendingAllowanceSnapshots, error); rejectAll(state.pendingPermissionAuthorizationStatuses, error); rejectAll(state.pendingPermissionAuthorizationStatusBatches, error); rejectAll(state.pendingSetPermissionAuthorizationStatuses, error); @@ -831,10 +891,12 @@ function sendWorkerRequest( function sendSessionActivationRequest( state: RuntimeState, buildMessage: (requestId: number) => MainToWorker, + changesIdentity = true, ): Promise { if (state.disposed) { return Promise.reject(state.closedError ?? new Error("runtime disposed")); } + if (changesIdentity) invalidateAllowanceIdentity(state); return sendWorkerRequest( state, state.pendingSessionActivations, @@ -844,6 +906,111 @@ function sendSessionActivationRequest( ); } +function sendLocalIdentityRequest( + state: RuntimeState, + buildMessage: (requestId: number) => MainToWorker, + onProgress?: (progress: LocalIdentityProgress) => void, +): Promise { + if (state.disposed) { + return Promise.reject(state.closedError ?? new Error("runtime disposed")); + } + const generation = state.identityGeneration; + const { promise, resolve, reject } = Promise.withResolvers(); + const requestId = ++nextLocalIdentityRequestId; + state.pendingLocalIdentities.set(requestId, { + resolve(identity) { + if (generation !== state.identityGeneration || state.disposePending) { + reject(new Error("local identity activation changed")); + return; + } + state.identityAccountId = identity.identityAccountId; + resolve(identity); + }, + reject, + onProgress, + }); + try { + state.worker.postMessage(buildMessage(requestId)); + } catch (error) { + state.pendingLocalIdentities.delete(requestId); + reject(error); + } + return promise; +} + +function invalidateAllowanceIdentity(state: RuntimeState): void { + state.identityGeneration++; + state.identityAccountId = null; + rejectAll( + state.pendingAllowanceSnapshots, + new Error("local identity activation changed"), + ); +} + +async function getWalletAllowanceSnapshot( + state: RuntimeState, + input: string[], +): Promise { + if (state.disposed || state.disposePending) { + throw state.closedError ?? new Error("runtime disposed"); + } + if ( + state.role !== "signing" || + state.pendingSessionActivations.size > 0 || + state.pendingDisconnects.size > 0 + ) { + throw new Error( + "allowance inspection requires a current local signing identity", + ); + } + const productIds = validateAllowanceProductIds(input); + const accountId = state.identityAccountId; + const generation = state.identityGeneration; + const requestId = ++nextAllowanceSnapshotRequestId; + const { promise, resolve, reject } = + Promise.withResolvers(); + const timeout = setTimeout(() => { + state.pendingAllowanceSnapshots.delete(requestId); + reject(new Error("wallet allowance inspection timed out after 30000ms")); + }, 30_000); + state.pendingAllowanceSnapshots.set(requestId, { + resolve(snapshot) { + clearTimeout(timeout); + if ( + generation !== state.identityGeneration || + snapshot?.schemaVersion !== 1 || + (accountId !== null && snapshot.identityAccountId !== accountId) || + snapshot.networkSuffix !== state.networkSuffix + ) { + reject( + new Error( + "wallet allowance snapshot does not match the current identity", + ), + ); + return; + } + resolve(snapshot); + }, + reject(error) { + clearTimeout(timeout); + reject(error); + }, + }); + try { + state.worker.postMessage({ + kind: "getWalletAllowanceSnapshot", + requestId, + productIds, + } satisfies MainToWorker); + } catch (error) { + settlePending(state.pendingAllowanceSnapshots, requestId, { + ok: false, + error: errorMessage(error), + }); + } + return promise; +} + function closeCoreState(core: CoreState, error: Error): void { if (core.disposed) return; core.disposed = true; @@ -903,9 +1070,9 @@ function teardown(state: RuntimeState, error: Error, fault: boolean): void { } } -export interface CreateWebWorkerPairingHostRuntimeOptions { +interface CreateWebWorkerHostRuntimeOptions { logLevel?: LogLevel; - hostConfig: WebWorkerHostConfig; + hostConfig: WebWorkerHostConfig | WebWorkerSigningHostConfig; initTimeoutMs?: number; /** * Host role the worker constructs. Omitted means `"pairing"`. @@ -921,6 +1088,14 @@ export interface CreateWebWorkerPairingHostRuntimeOptions { operationGraceMs?: number; } +export interface CreateWebWorkerPairingHostRuntimeOptions extends CreateWebWorkerHostRuntimeOptions { + hostConfig: WebWorkerHostConfig; +} + +export interface CreateWebWorkerSigningHostRuntimeOptions extends CreateWebWorkerHostRuntimeOptions { + hostConfig: WebWorkerSigningHostConfig; +} + export type WebWorkerHostCallbacks = RequiredHostCallbacks; export function createWebWorkerPairingHostRuntime( @@ -928,11 +1103,41 @@ export function createWebWorkerPairingHostRuntime( host: WebWorkerHostCallbacks, options: CreateWebWorkerPairingHostRuntimeOptions, ): Promise { + // No role default: a host that asks for none must put exactly what it put + // on the wire before the field existed, and the worker reads absent as + // "pairing". + return createWebWorkerHostRuntime(worker, host, options); +} + +export function createWebWorkerSigningHostRuntime( + worker: Worker, + host: WebWorkerHostCallbacks, + options: CreateWebWorkerSigningHostRuntimeOptions, +): Promise { + return createWebWorkerHostRuntime(worker, host, { + ...options, + role: options.role ?? "signing", + }); +} + +function createWebWorkerHostRuntime( + worker: Worker, + host: WebWorkerHostCallbacks, + options: CreateWebWorkerHostRuntimeOptions, +): Promise { const callbacks = createWasmRawCallbacks(host); return new Promise((resolve, reject) => { const state: RuntimeState = { worker, + role: options.role ?? "pairing", + networkSuffix: + "networkSuffix" in options.hostConfig + ? options.hostConfig.networkSuffix + : undefined, + identityAccountId: null, + identityGeneration: 0, + pendingAllowanceSnapshots: new Map(), rawCallbacks: callbacks, cores: new Map(), pendingCores: new Map(), @@ -944,6 +1149,7 @@ export function createWebWorkerPairingHostRuntime( chainConnections: new Map(), pendingDisconnects: new Map(), pendingSessionActivations: new Map(), + pendingLocalIdentities: new Map(), pendingPermissionAuthorizationStatuses: new Map(), pendingPermissionAuthorizationStatusBatches: new Map(), pendingSetPermissionAuthorizationStatuses: new Map(), @@ -962,7 +1168,8 @@ export function createWebWorkerPairingHostRuntime( coreWireSchemaHash: undefined, }; - let runtime: WorkerPairingHostRuntime | null = null; + let runtime: (WorkerPairingHostRuntime & WorkerSigningHostRuntime) | null = + null; const notifyFault = (error: Error): void => { teardown(state, error, true); @@ -1005,6 +1212,34 @@ export function createWebWorkerPairingHostRuntime( case "sessionActivationResponse": handleSessionActivationResponse(state, msg); break; + case "localIdentityProgress": + if (state.disposed) break; + try { + state.pendingLocalIdentities + .get(msg.requestId) + ?.onProgress?.(msg.progress); + } catch { + // UI observers cannot fail or settle an identity operation. + } + break; + case "localIdentityResponse": + settlePending( + state.pendingLocalIdentities, + msg.requestId, + msg.ok + ? { ok: true, value: msg.identity } + : { ok: false, error: msg.error }, + ); + break; + case "walletAllowanceSnapshotResponse": + settlePending( + state.pendingAllowanceSnapshots, + msg.requestId, + msg.ok + ? { ok: true, value: msg.snapshot } + : { ok: false, error: msg.error }, + ); + break; case "permissionAuthorizationStatusResponse": handlePermissionAuthorizationStatusResponse(state, msg); break; @@ -1137,13 +1372,13 @@ export function createWebWorkerPairingHostRuntime( kind: "init", logLevel: devLogLevelOverride ?? options.logLevel ?? "off", hostConfig: options.hostConfig, + role: options.role, capabilities: { chat: host.chat !== undefined, permissionStatus: host.permissionStatus !== undefined, pocket: host.pocket !== undefined, }, debuggerUrl: debuggerEnablement.url, - role: options.role, } satisfies MainToWorker); } else if (msg.kind === "ready") { state.coreWireSchemaHash = msg.schema; @@ -1236,8 +1471,10 @@ function handleFrameError( } } -function buildRuntime(state: RuntimeState): WorkerPairingHostRuntime { - const runtime: WorkerPairingHostRuntime = { +function buildRuntime( + state: RuntimeState, +): WorkerPairingHostRuntime & WorkerSigningHostRuntime { + const runtime: WorkerPairingHostRuntime & WorkerSigningHostRuntime = { coreWireSchemaHash: state.coreWireSchemaHash, createProvider(product): Promise { if (state.disposed) { @@ -1265,6 +1502,7 @@ function buildRuntime(state: RuntimeState): WorkerPairingHostRuntime { }); }, disconnectSession(): Promise { + invalidateAllowanceIdentity(state); return sendWorkerRequest( state, state.pendingDisconnects, @@ -1378,18 +1616,58 @@ function buildRuntime(state: RuntimeState): WorkerPairingHostRuntime { })); }, setGrantAllowancesUnchecked(granted: boolean): Promise { + return sendSessionActivationRequest( + state, + (requestId) => ({ + kind: "setGrantAllowancesUnchecked", + requestId, + granted, + }), + false, + ); + }, + resetSessionState(): Promise { return sendSessionActivationRequest(state, (requestId) => ({ - kind: "setGrantAllowancesUnchecked", + kind: "resetSessionState", requestId, - granted, })); }, - resetSessionState(): Promise { + activateLocalSessionWithIdentity( + secret: Uint8Array, + liteUsername?: string, + ): Promise { return sendSessionActivationRequest(state, (requestId) => ({ - kind: "resetSessionState", + kind: "activateLocalSessionWithIdentity", requestId, + secret, + liteUsername, })); }, + refreshLocalIdentity(): Promise { + return sendLocalIdentityRequest(state, (requestId) => ({ + kind: "refreshLocalIdentity", + requestId, + })); + }, + getWalletAllowanceSnapshot(productIds): Promise { + return getWalletAllowanceSnapshot(state, productIds); + }, + registerLocalLiteUsername( + baseUsername, + identityBackendBaseUrl, + onProgress, + ): Promise { + return sendLocalIdentityRequest( + state, + (requestId) => ({ + kind: "registerLocalLiteUsername", + requestId, + baseUsername, + identityBackendBaseUrl, + }), + onProgress, + ); + }, getPermissionAuthorizationStatus(productId, request) { return sendWorkerRequest( state, @@ -1442,6 +1720,7 @@ function buildRuntime(state: RuntimeState): WorkerPairingHostRuntime { } satisfies MainToWorker); }, dispose(): void { + invalidateAllowanceIdentity(state); devGlobalTargets.delete(runtime); // Let a background task (e.g. a funding transaction) finish; the last // endOperation runs the teardown. Fault teardown is never deferred. diff --git a/js/packages/truapi-host/src/web/index.ts b/js/packages/truapi-host/src/web/index.ts index acba3a71a..7db4a1087 100644 --- a/js/packages/truapi-host/src/web/index.ts +++ b/js/packages/truapi-host/src/web/index.ts @@ -2,8 +2,31 @@ export type { IframeHost, IframeHostOptions } from "./create-iframe-host.js"; export { createIframeHost } from "./create-iframe-host.js"; export type { CreateWebWorkerPairingHostRuntimeOptions, + CreateWebWorkerSigningHostRuntimeOptions, WebWorkerHostConfig, + WebWorkerSigningHostConfig, WebWorkerHostCallbacks, WorkerPairingHostRuntime, + WorkerSigningHostRuntime, } from "./create-worker-host-runtime.js"; -export { createWebWorkerPairingHostRuntime } from "./create-worker-host-runtime.js"; +export { + createWebWorkerPairingHostRuntime, + createWebWorkerSigningHostRuntime, +} from "./create-worker-host-runtime.js"; +export type { + LocalIdentity, + LocalIdentityProgress, +} from "../worker-protocol.js"; +export type { + AllowanceCollection, + AllowanceObservation, + AllowanceSection, + AllowanceSlot, + AllowancePool, + AllowanceClaims, + StatementAllowanceSnapshot, + PgasClaimsSnapshot, + PgasBalancesSnapshot, + BulletinQuota, + WalletAllowanceSnapshot, +} from "../wallet-allowances.js"; diff --git a/js/packages/truapi-host/src/web/worker-provider.test.ts b/js/packages/truapi-host/src/web/worker-provider.test.ts index a61df2354..855f8c468 100644 --- a/js/packages/truapi-host/src/web/worker-provider.test.ts +++ b/js/packages/truapi-host/src/web/worker-provider.test.ts @@ -30,8 +30,16 @@ import type { WorkerDemandChange, } from "../runtime.js"; import { makeHostCallbacks, settle } from "../test-support.js"; -import { createWebWorkerPairingHostRuntime } from "./index.js"; -import type { CreateWebWorkerPairingHostRuntimeOptions } from "./index.js"; +import { + createWebWorkerPairingHostRuntime, + createWebWorkerSigningHostRuntime, +} from "./index.js"; +import type { + CreateWebWorkerPairingHostRuntimeOptions, + CreateWebWorkerSigningHostRuntimeOptions, + LocalIdentityProgress, + WorkerSigningHostRuntime, +} from "./index.js"; type WorkerMessage = Record; @@ -208,6 +216,22 @@ async function readyRuntime(worker: FakeWorker) { return runtimePromise; } +async function readySigningRuntime(worker: FakeWorker) { + const runtimePromise = createWebWorkerSigningHostRuntime( + asWorker(worker), + makeHostCallbacks(), + { + hostConfig: { + ...hostConfigFromRuntimeConfig(runtimeConfig()), + networkSuffix: "paseo", + }, + }, + ); + worker.emit({ kind: "loaded" }); + worker.emit({ kind: "ready" }); + return runtimePromise; +} + async function readyProvider(worker: FakeWorker, options: ReadyOptions = {}) { const providerPromise = createProviderFromRuntime( asWorker(worker), @@ -247,6 +271,9 @@ describe("createWebWorkerPairingHostRuntime", () => { kind: "init", logLevel: "debug", hostConfig: hostConfigFromRuntimeConfig(config), + // A host that asks for no role sends none: the worker reads absent as + // "pairing", so the message stays what it was before the field existed. + role: undefined, capabilities: { chat: false, permissionStatus: false, pocket: false }, debuggerUrl: null, }); @@ -266,6 +293,143 @@ describe("createWebWorkerPairingHostRuntime", () => { provider.dispose(); }); + it("activates a browser-local signing session in the worker", async () => { + const worker = new FakeWorker(); + const hostConfig = { + ...hostConfigFromRuntimeConfig(runtimeConfig()), + networkSuffix: "paseo", + } satisfies CreateWebWorkerSigningHostRuntimeOptions["hostConfig"]; + const runtimePromise = createWebWorkerSigningHostRuntime( + asWorker(worker), + makeHostCallbacks(), + { hostConfig }, + ); + + worker.emit({ kind: "loaded" }); + expect(worker.messages[0]).toMatchObject({ + kind: "init", + hostConfig, + role: "signing", + }); + worker.emit({ kind: "ready" }); + const runtime = await runtimePromise; + + const secret = new Uint8Array(32).fill(7); + const activation = runtime.activateLocalSession(secret); + const request = lastMessageOfKind(worker, "activateLocalSession"); + expect(request).toMatchObject({ + kind: "activateLocalSession", + secret, + }); + worker.emit({ + kind: "sessionActivationResponse", + requestId: request.requestId, + ok: true, + }); + await activation; + runtime.dispose(); + }); + + it("scopes identity progress to pending requests and isolates observer failures", async () => { + const worker = new FakeWorker(); + const runtime = await readySigningRuntime(worker); + const progress: LocalIdentityProgress[] = []; + const identity = { + identityAccountId: `0x${"11".repeat(32)}`, + liteUsername: "alice.paseo", + }; + const claim = runtime.registerLocalLiteUsername( + "alice", + "https://identity.invalid", + (event) => { + progress.push(event); + throw new Error("observer failed"); + }, + ); + const request = lastMessageOfKind(worker, "registerLocalLiteUsername"); + let settled = false; + void claim.then(() => { + settled = true; + }); + worker.emit({ + kind: "localIdentityProgress", + requestId: request.requestId, + progress: { stage: "confirming" }, + }); + await settle(); + expect(settled).toBe(false); + worker.emit({ + kind: "localIdentityResponse", + requestId: request.requestId, + ok: true, + identity, + }); + await expect(claim).resolves.toEqual(identity); + + const nextProgress: LocalIdentityProgress[] = []; + const nextClaim = runtime.registerLocalLiteUsername( + "bob", + "https://identity.invalid", + (event) => nextProgress.push(event), + ); + const next = lastMessageOfKind(worker, "registerLocalLiteUsername"); + worker.emit({ + kind: "localIdentityProgress", + requestId: request.requestId, + progress: { stage: "retrying", error: "late response" }, + }); + worker.emit({ + kind: "localIdentityProgress", + requestId: next.requestId, + progress: { stage: "checking" }, + }); + worker.emit({ + kind: "localIdentityResponse", + requestId: next.requestId, + ok: false, + error: "claim rejected", + }); + await expect(nextClaim).rejects.toThrow("claim rejected"); + worker.emit({ + kind: "localIdentityProgress", + requestId: next.requestId, + progress: { stage: "confirming" }, + }); + expect(progress).toEqual([{ stage: "confirming" }]); + expect(nextProgress).toEqual([{ stage: "checking" }]); + runtime.dispose(); + }); + + for (const close of ["dispose", "fault"] as const) { + it(`ignores late identity progress after runtime ${close}`, async () => { + const worker = new FakeWorker(); + const runtime = await readySigningRuntime(worker); + const progress: LocalIdentityProgress[] = []; + const claim = runtime.registerLocalLiteUsername( + "alice", + "https://identity.invalid", + (event) => progress.push(event), + ); + const request = lastMessageOfKind(worker, "registerLocalLiteUsername"); + worker.emit({ + kind: "localIdentityProgress", + requestId: request.requestId, + progress: { stage: "confirming" }, + }); + if (close === "dispose") runtime.dispose(); + else worker.emitError("worker stopped"); + await expect(claim).rejects.toThrow( + close === "dispose" ? "runtime disposed" : "worker stopped", + ); + worker.emit({ + kind: "localIdentityProgress", + requestId: request.requestId, + progress: { stage: "retrying", error: "late response" }, + }); + expect(progress).toEqual([{ stage: "confirming" }]); + }); + } + it("reports the chat capability to the worker when the host serves it", async () => { const worker = new FakeWorker(); void createWebWorkerPairingHostRuntime( @@ -1922,3 +2086,146 @@ describe("worker host role", () => { await finishProviderReady(worker, providerPromise).catch(() => {}); }); }); + +describe("wallet allowance inspection isolation", () => { + const account = `0x${"11".repeat(32)}`; + + async function resolvedRuntime(worker: FakeWorker) { + const runtime = await readySigningRuntime(worker); + const identity = runtime.refreshLocalIdentity(); + worker.emit({ + kind: "localIdentityResponse", + requestId: lastMessageOfKind(worker, "refreshLocalIdentity").requestId, + ok: true, + identity: { identityAccountId: account, liteUsername: "alice.paseo" }, + }); + await identity; + return runtime; + } + + it("denies wallet-wide inspection to pairing hosts", async () => { + const pairingWorker = new FakeWorker(); + const pairing = await readyRuntime(pairingWorker); + await expect( + ( + pairing as unknown as WorkerSigningHostRuntime + ).getWalletAllowanceSnapshot([]), + ).rejects.toThrow("local signing identity"); + expect(indexOfKind(pairingWorker, "getWalletAllowanceSnapshot")).toBe(-1); + pairing.dispose(); + }); + + it("rejects an oversized or ambiguous product scope before dispatch", async () => { + const worker = new FakeWorker(); + const runtime = await resolvedRuntime(worker); + for (const ids of [ + ["a.dot", "a.dot"], + [""], + Array.from({ length: 33 }, (_, i) => `app${i}.dot`), + ]) { + await expect(runtime.getWalletAllowanceSnapshot(ids)).rejects.toThrow( + "product IDs", + ); + } + expect(indexOfKind(worker, "getWalletAllowanceSnapshot")).toBe(-1); + runtime.dispose(); + }); + + it.each(["dispose", "error", "messageerror"] as const)( + "settles pending reads when the worker closes through %s", + async (close) => { + const worker = new FakeWorker(); + const runtime = await resolvedRuntime(worker); + const read = runtime.getWalletAllowanceSnapshot([]); + const rejected = read.catch((error: unknown) => error); + if (close === "dispose") runtime.dispose(); + else if (close === "error") worker.emitError("worker stopped"); + else worker.emitMessageError(); + expect(await rejected).toBeInstanceOf(Error); + await expect(runtime.getWalletAllowanceSnapshot([])).rejects.toThrow(); + runtime.dispose(); + }, + ); + + it("rejects an in-flight snapshot immediately when another wallet activates", async () => { + const worker = new FakeWorker(); + const runtime = await resolvedRuntime(worker); + const read = runtime.getWalletAllowanceSnapshot([]); + const requestId = lastMessageOfKind( + worker, + "getWalletAllowanceSnapshot", + ).requestId; + const rejected = read.catch((error: unknown) => error); + const activation = runtime.activateLocalSession(new Uint8Array(32)); + await expect(runtime.getWalletAllowanceSnapshot([])).rejects.toThrow( + "local signing identity", + ); + expect(await rejected).toBeInstanceOf(Error); + worker.emit({ + kind: "walletAllowanceSnapshotResponse", + requestId, + ok: true, + snapshot: { + schemaVersion: 1, + identityAccountId: account, + networkSuffix: "paseo", + }, + }); + worker.emit({ + kind: "sessionActivationResponse", + requestId: lastMessageOfKind(worker, "activateLocalSession").requestId, + ok: true, + }); + await activation; + runtime.dispose(); + }); + + it("does not accept a late identity refresh from a different activation", async () => { + const worker = new FakeWorker(); + const runtime = await readySigningRuntime(worker); + const identity = runtime.refreshLocalIdentity(); + const rejected = identity.catch((error: unknown) => error); + const activation = runtime.activateLocalSession(new Uint8Array(32)); + worker.emit({ + kind: "localIdentityResponse", + requestId: lastMessageOfKind(worker, "refreshLocalIdentity").requestId, + ok: true, + identity: { identityAccountId: account }, + }); + worker.emit({ + kind: "sessionActivationResponse", + requestId: lastMessageOfKind(worker, "activateLocalSession").requestId, + ok: true, + }); + expect(await rejected).toBeInstanceOf(Error); + await activation; + runtime.dispose(); + }); + + it("retains wallet binding when only the debug allocation policy changes", async () => { + const worker = new FakeWorker(); + const runtime = await resolvedRuntime(worker); + const toggle = runtime.setGrantAllowancesUnchecked(false); + worker.emit({ + kind: "sessionActivationResponse", + requestId: lastMessageOfKind(worker, "setGrantAllowancesUnchecked") + .requestId, + ok: true, + }); + await toggle; + const read = runtime.getWalletAllowanceSnapshot([]); + worker.emit({ + kind: "walletAllowanceSnapshotResponse", + requestId: lastMessageOfKind(worker, "getWalletAllowanceSnapshot") + .requestId, + ok: true, + snapshot: { + schemaVersion: 1, + identityAccountId: `0x${"22".repeat(32)}`, + networkSuffix: "paseo", + }, + }); + await expect(read).rejects.toThrow("current identity"); + runtime.dispose(); + }); +}); diff --git a/js/packages/truapi-host/src/worker-local-identity.test.ts b/js/packages/truapi-host/src/worker-local-identity.test.ts new file mode 100644 index 000000000..c5e2867a4 --- /dev/null +++ b/js/packages/truapi-host/src/worker-local-identity.test.ts @@ -0,0 +1,134 @@ +import { afterEach, expect, spyOn, test } from "bun:test"; +import { resolveLocalIdentity } from "./worker-local-identity.js"; +import type { WorkerSigningHostRuntime } from "./wasm-module.js"; +import type { LocalIdentityProgress } from "./worker-protocol.js"; + +const account = `0x${"11".repeat(32)}`; +const registration = { + baseUsername: "lateconfirmation", + identityBackendBaseUrl: "https://identity.invalid/api/v1", +}; +const spies: { mockRestore(): void }[] = []; +afterEach(() => { + for (const spy of spies.splice(0)) spy.mockRestore(); +}); + +function setup( + refresh: () => Promise<{ identityAccountId: string; liteUsername?: string }>, +) { + let submissions = 0; + const realTimeout = globalThis.setTimeout; + spies.push( + spyOn(globalThis, "setTimeout").mockImplementation((( + callback: () => void, + delay?: number, + ) => + realTimeout(callback, delay === 4_000 ? 0 : delay)) as typeof setTimeout), + ); + spies.push( + spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const path = new URL(String(input)).pathname; + if (path.endsWith("/attester")) + return Response.json({ attester: account }); + if (path.endsWith("/auth/challenges")) + return Response.json({ challenge: "AA==" }); + if (path.endsWith("/auth/token")) + return Response.json({ token: "test-proof-token" }); + if (path.endsWith("/usernames")) { + submissions++; + return new Response("", { status: 202 }); + } + throw new Error(`Unexpected request: ${path}`); + }), + ); + const runtime = { + localIdentityContext: () => ({ + activationId: 1, + identityAccountId: account, + }), + refreshLocalIdentity: refresh, + localIdentityAuthProof: () => new Uint8Array(64), + localLiteRegistrationBody: async () => "{}", + } as unknown as WorkerSigningHostRuntime; + return { runtime, submissions: () => submissions }; +} + +test("an accepted claim confirms after the old polling cutoff without resubmission", async () => { + let reads = 0; + const confirmed = { + identityAccountId: account, + liteUsername: "lateconfirmation.paseo", + }; + const { runtime, submissions } = setup(async () => { + reads++; + if (reads === 32) throw new Error("temporary chain disconnect"); + return reads > 40 ? confirmed : { identityAccountId: account }; + }); + await expect( + resolveLocalIdentity(runtime, new AbortController().signal, registration), + ).resolves.toEqual(confirmed); + expect(submissions()).toBe(1); +}); + +test("accepted registration reports read retries and recovers without another submission", async () => { + const progress: LocalIdentityProgress[] = []; + const confirmed = { + identityAccountId: account, + liteUsername: "lateconfirmation.paseo", + }; + let reads = 0; + const { runtime, submissions } = setup(async () => { + reads++; + if (reads === 2) throw new Error("temporary chain disconnect"); + return reads === 4 ? confirmed : { identityAccountId: account }; + }); + await expect( + resolveLocalIdentity( + runtime, + new AbortController().signal, + registration, + (event) => { + progress.push(event); + throw new Error("observer failed"); + }, + ), + ).resolves.toEqual(confirmed); + expect(progress).toEqual([ + { stage: "checking" }, + { stage: "authenticating" }, + { stage: "submitting" }, + { stage: "confirming" }, + { stage: "retrying", error: "temporary chain disconnect" }, + { stage: "confirming" }, + { stage: "confirming" }, + ]); + expect(submissions()).toBe(1); +}); + +test("disposing the wallet stops confirmation monitoring without resubmission", async () => { + const controller = new AbortController(); + let reads = 0; + const { runtime, submissions } = setup(async () => { + if (++reads === 4) controller.abort(new Error("wallet disposed")); + return { identityAccountId: account }; + }); + await expect( + resolveLocalIdentity(runtime, controller.signal, registration), + ).rejects.toThrow("wallet disposed"); + expect(submissions()).toBe(1); +}); + +test("confirmation from a different identity is rejected", async () => { + let reads = 0; + const { runtime } = setup(async () => + ++reads === 1 + ? { identityAccountId: account } + : { + identityAccountId: `0x${"22".repeat(32)}`, + liteUsername: "wrong.paseo", + }, + ); + await expect( + resolveLocalIdentity(runtime, new AbortController().signal, registration), + ).rejects.toThrow("verified identity does not match"); +}); diff --git a/js/packages/truapi-host/src/worker-local-identity.ts b/js/packages/truapi-host/src/worker-local-identity.ts new file mode 100644 index 000000000..161261910 --- /dev/null +++ b/js/packages/truapi-host/src/worker-local-identity.ts @@ -0,0 +1,187 @@ +import { errorMessage } from "./error.js"; +import type { + LocalIdentity, + LocalIdentityProgress, +} from "./worker-protocol.js"; +import type { WorkerSigningHostRuntime } from "./wasm-module.js"; + +function stringField(value: unknown, field: string): string { + if (typeof value !== "object" || value === null || !(field in value)) { + throw new Error(`identity backend response missing ${field}`); + } + const result: unknown = Reflect.get(value, field); + if (typeof result !== "string" || !result.length) { + throw new Error(`identity backend response has invalid ${field}`); + } + return result; +} + +function delay(milliseconds: number, signal: AbortSignal): Promise { + signal.throwIfAborted(); + const { promise, resolve, reject } = Promise.withResolvers(); + const abort = () => { + clearTimeout(timer); + reject(signal.reason); + }; + const timer = setTimeout(() => { + signal.removeEventListener("abort", abort); + resolve(); + }, milliseconds); + signal.addEventListener("abort", abort, { once: true }); + return promise; +} + +/** HTTP stays in the worker; all secret material and proof construction stay native. */ +export async function resolveLocalIdentity( + runtime: WorkerSigningHostRuntime, + signal: AbortSignal, + registration?: { baseUsername: string; identityBackendBaseUrl: string }, + onProgress?: (progress: LocalIdentityProgress) => void, +): Promise { + const context = runtime.localIdentityContext(); + const check = () => { + signal.throwIfAborted(); + if (runtime.localIdentityContext().activationId !== context.activationId) { + throw new Error("local identity activation changed"); + } + }; + const refresh = async () => { + check(); + const identity = await runtime.refreshLocalIdentity(context.activationId); + check(); + if (identity.identityAccountId !== context.identityAccountId) { + throw new Error( + "verified identity does not match the active UID account", + ); + } + return identity; + }; + const reportProgress = (progress: LocalIdentityProgress) => { + try { + onProgress?.(progress); + } catch { + // Observers must not interrupt authentication, submission, or polling. + } + }; + reportProgress({ stage: "checking" }); + const existing = await refresh(); + if (!registration || existing.liteUsername) return existing; + + const base = registration.identityBackendBaseUrl.replace(/\/+$/, ""); + if (!base) throw new Error("identity backend base URL is empty"); + const request = async ( + path: string, + init: RequestInit, + ): Promise => { + check(); + const response = await fetch(`${base}${path}`, { + ...init, + credentials: "omit", + redirect: "error", + signal: AbortSignal.any([signal, AbortSignal.timeout(30_000)]), + }); + check(); + return response; + }; + const json = async (path: string, init: RequestInit): Promise => { + const response = await request(path, init); + const text = await response.text(); + check(); + if (!response.ok) { + throw new Error( + `identity backend ${path} failed (${response.status}): ${text}`, + ); + } + return JSON.parse(text) as unknown; + }; + const headers = { "Content-Type": "application/json" }; + reportProgress({ stage: "authenticating" }); + const attester = stringField( + await json("/attester", { method: "GET" }), + "attester", + ); + const verifierHex = attester.replace(/^0x/, ""); + if (!/^[0-9a-fA-F]{64}$/.test(verifierHex)) { + throw new Error("identity backend attester must be 32-byte hex"); + } + const verifier = Uint8Array.from(verifierHex.match(/../g)!, (byte) => + parseInt(byte, 16), + ); + const challenge = stringField( + await json("/auth/challenges", { method: "POST", headers, body: "{}" }), + "challenge", + ); + const challengeBytes = Uint8Array.from(atob(challenge), (char) => + char.charCodeAt(0), + ); + check(); + const proof = runtime.localIdentityAuthProof( + context.activationId, + challengeBytes, + ); + const token = stringField( + await json("/auth/token", { + method: "POST", + headers: { + ...headers, + "Auth-ClientId": btoa(String.fromCharCode(...proof.subarray(0, 32))), + "Auth-ClientProof": btoa(String.fromCharCode(...proof.subarray(32))), + "Auth-Challenge": challenge, + }, + body: "{}", + }), + "token", + ); + check(); + const body = await runtime.localLiteRegistrationBody( + context.activationId, + registration.baseUsername, + verifier, + ); + check(); + reportProgress({ stage: "submitting" }); + const response = await request("/usernames", { + method: "POST", + headers: { ...headers, Authorization: `Bearer ${token}` }, + body, + }); + const responseText = await response.text(); + check(); + if (!response.ok) { + throw new Error( + `username registration failed (${response.status}): ${responseText}`, + ); + } + reportProgress({ stage: "confirming" }); + + // Backend acceptance is not chain confirmation. Keep observing this activation + // until ownership is verified or the caller disposes it; never resubmit a claim + // just because indexing/finality takes longer than a fixed polling window. + for (;;) { + check(); + let identity: LocalIdentity; + try { + identity = await runtime.refreshLocalIdentity(context.activationId); + } catch (error) { + check(); + let message = "Chain read failed"; + try { + message = errorMessage(error); + } catch { + // An unprintable thrown value must not stop confirmation polling. + } + reportProgress({ stage: "retrying", error: message }); + await delay(4_000, signal); + continue; + } + check(); + if (identity.identityAccountId !== context.identityAccountId) { + throw new Error( + "verified identity does not match the active UID account", + ); + } + reportProgress({ stage: "confirming" }); + if (identity.liteUsername) return identity; + await delay(4_000, signal); + } +} diff --git a/js/packages/truapi-host/src/worker-protocol.ts b/js/packages/truapi-host/src/worker-protocol.ts index 0581c3cce..ac2120870 100644 --- a/js/packages/truapi-host/src/worker-protocol.ts +++ b/js/packages/truapi-host/src/worker-protocol.ts @@ -31,6 +31,7 @@ import type { OptionalCapabilities } from "./generated/worker-callbacks.js"; import type { LogLevel, PermissionAuthorizationStatus } from "./runtime.js"; +import type { WalletAllowanceSnapshot } from "./wallet-allowances.js"; import type { CallbackName, SubscriptionName, @@ -50,6 +51,18 @@ export type { */ export type CallbackArgs = readonly unknown[]; +/** Chain-verified identity metadata for the wallet's network-specific UID account. */ +export interface LocalIdentity { + /** Canonical lowercase 0x-prefixed 32-byte account identifier. */ + identityAccountId: string; + liteUsername?: string; +} + +/** Observable registration stages; acceptance is not verified ownership. */ +export type LocalIdentityProgress = + | { stage: "checking" | "authenticating" | "submitting" | "confirming" } + | { stage: "retrying"; error: string }; + /** * Messages posted by the main window to the WASM worker. These either control * worker/core lifecycle, forward encoded TrUAPI frames into the core, or return @@ -114,6 +127,24 @@ export type MainToWorker = granted: boolean; } | { kind: "resetSessionState"; requestId: number } + | { + kind: "activateLocalSessionWithIdentity"; + requestId: number; + secret: Uint8Array; + liteUsername?: string; + } + | { kind: "refreshLocalIdentity"; requestId: number } + | { + kind: "getWalletAllowanceSnapshot"; + requestId: number; + productIds: string[]; + } + | { + kind: "registerLocalLiteUsername"; + requestId: number; + baseUsername: string; + identityBackendBaseUrl: string; + } | { kind: "getPermissionAuthorizationStatus"; productId: string; @@ -214,6 +245,35 @@ export type WorkerToMain = ok: false; error: string; } + | { + kind: "localIdentityProgress"; + requestId: number; + progress: LocalIdentityProgress; + } + | { + kind: "localIdentityResponse"; + requestId: number; + ok: true; + identity: LocalIdentity; + } + | { + kind: "localIdentityResponse"; + requestId: number; + ok: false; + error: string; + } + | { + kind: "walletAllowanceSnapshotResponse"; + requestId: number; + ok: true; + snapshot: WalletAllowanceSnapshot; + } + | { + kind: "walletAllowanceSnapshotResponse"; + requestId: number; + ok: false; + error: string; + } | { kind: "permissionAuthorizationStatusResponse"; requestId: number; diff --git a/js/packages/truapi-host/src/worker-runtime.ts b/js/packages/truapi-host/src/worker-runtime.ts index 3c59bbb14..aef12a21c 100644 --- a/js/packages/truapi-host/src/worker-runtime.ts +++ b/js/packages/truapi-host/src/worker-runtime.ts @@ -23,12 +23,18 @@ import { } from "./worker-permission-authorization.js"; import type { WasmModuleShape, + WorkerHostRuntime, WorkerPairingHostRuntime, - WorkerSigningHostRuntime, WorkerProductRuntime, + WorkerSigningHostRuntime, WorkerTransition, } from "./wasm-module.js"; import { errorMessage } from "./error.js"; +import { resolveLocalIdentity } from "./worker-local-identity.js"; +import { + validateAllowanceProductIds, + validateWalletAllowanceSnapshot, +} from "./wallet-allowances.js"; import { CHAT_ACTION_ENTRY_POINT, RENDERER_ACTION_ENTRY_POINT, @@ -634,7 +640,7 @@ function buildCoreCallbacks(coreId: number) { }; } -let runtime: WorkerPairingHostRuntime | null = null; +let runtime: WorkerHostRuntime | null = null; const cores = new Map(); // Outstanding receiveFrame calls per core. wasm-bindgen holds a borrow of the // core for the whole duration of an async method, so `free()` throws while one @@ -643,6 +649,127 @@ const inFlightFrames = new Map>>(); /** Live render subscriptions, keyed by main-thread render id. */ const renders: RenderSubscriptions = new Map(); let wasm: WasmModuleShape | null = null; +let identityAbort: AbortController | null = null; +const identityOperations = new Set>(); +let allowanceNetworkSuffix: string | null = null; +let allowanceGeneration = 0; +const allowanceOperations = new Set>(); + +function handleWalletAllowanceSnapshot( + requestId: number, + input: string[], +): void { + const rt = runtime; + const generation = allowanceGeneration; + const operation = (async () => { + try { + if (!rt || !isSigningRuntime(rt) || allowanceNetworkSuffix === null) { + throw new Error( + "wallet allowance inspection requires a signing runtime", + ); + } + const productIds = validateAllowanceProductIds(input); + const context = rt.localIdentityContext(); + const snapshot = await rt.getWalletAllowanceSnapshot( + context.activationId, + productIds, + ); + if ( + runtime !== rt || + generation !== allowanceGeneration || + rt.localIdentityContext().activationId !== context.activationId + ) { + throw new Error( + "local identity activation changed during allowance inspection", + ); + } + validateWalletAllowanceSnapshot( + snapshot, + context.identityAccountId, + allowanceNetworkSuffix, + productIds, + ); + postToMain({ + kind: "walletAllowanceSnapshotResponse", + requestId, + ok: true, + snapshot, + }); + } catch (error) { + postToMain({ + kind: "walletAllowanceSnapshotResponse", + requestId, + ok: false, + error: errorMessage(error), + }); + } + })(); + allowanceOperations.add(operation); + void operation.finally(() => allowanceOperations.delete(operation)); +} + +function handleLocalIdentity( + requestId: number, + registration?: { baseUsername: string; identityBackendBaseUrl: string }, +): void { + const rt = runtime; + if (!rt || !isSigningRuntime(rt) || identityAbort) { + postToMain({ + kind: "localIdentityResponse", + requestId, + ok: false, + error: identityAbort + ? "local identity operation already in progress" + : "signing runtime is not active", + }); + return; + } + const controller = new AbortController(); + identityAbort = controller; + const operation = (async () => { + try { + const identity = await resolveLocalIdentity( + rt, + controller.signal, + registration, + (progress) => { + if (controller.signal.aborted || identityAbort !== controller) return; + postToMain({ kind: "localIdentityProgress", requestId, progress }); + }, + ); + controller.signal.throwIfAborted(); + postToMain({ + kind: "localIdentityResponse", + requestId, + ok: true, + identity, + }); + } catch (error) { + postToMain({ + kind: "localIdentityResponse", + requestId, + ok: false, + error: errorMessage(error), + }); + } finally { + if (identityAbort === controller) identityAbort = null; + } + })(); + identityOperations.add(operation); + void operation.finally(() => identityOperations.delete(operation)); +} + +function isPairingRuntime( + candidate: WorkerHostRuntime, +): candidate is WorkerPairingHostRuntime { + return "cancelPairing" in candidate; +} + +function isSigningRuntime( + candidate: WorkerHostRuntime, +): candidate is WorkerSigningHostRuntime { + return "activateLocalSession" in candidate; +} (async () => { try { @@ -696,6 +823,10 @@ ctx.addEventListener("message", (ev: MessageEvent) => { }); break; } + const hostConfig = msg.hostConfig as { networkSuffix?: unknown }; + if (typeof hostConfig?.networkSuffix === "string") { + allowanceNetworkSuffix = hostConfig.networkSuffix; + } runtime = new SigningRuntime( buildRawCallbacks(msg.capabilities), msg.hostConfig, @@ -742,10 +873,13 @@ ctx.addEventListener("message", (ev: MessageEvent) => { void handleFrame(msg.coreId, msg.bytes); break; case "disconnectSession": + identityAbort?.abort(new Error("local identity session disconnected")); void handleDisconnectSession(msg.requestId); break; case "cancelPairing": - runtime?.cancelPairing(); + if (runtime && isPairingRuntime(runtime)) { + runtime.cancelPairing(); + } break; case "getSessionChatIdentityKey": handleGetSessionChatIdentityKey(msg.requestId); @@ -764,7 +898,9 @@ ctx.addEventListener("message", (ev: MessageEvent) => { ); break; case "notifySessionStoreChanged": - runtime?.notifySessionStoreChanged(); + if (runtime && isPairingRuntime(runtime)) { + runtime.notifySessionStoreChanged(); + } break; case "acquireWorker": runtime?.acquireWorker(msg.productId); @@ -776,7 +912,10 @@ ctx.addEventListener("message", (ev: MessageEvent) => { void handleSessionActivation( msg.requestId, "activateStoredSession", - (rt) => rt.activateStoredSession(), + (rt) => + isPairingRuntime(rt) + ? rt.activateStoredSession() + : Promise.reject(new Error("pairing runtime is not active")), ); break; case "activateExternalSession": { @@ -784,7 +923,10 @@ ctx.addEventListener("message", (ev: MessageEvent) => { void handleSessionActivation( msg.requestId, "activateExternalSession", - (rt) => rt.activateExternalSession(blob), + (rt) => + isPairingRuntime(rt) + ? rt.activateExternalSession(blob) + : Promise.reject(new Error("pairing runtime is not active")), ); break; } @@ -839,14 +981,39 @@ ctx.addEventListener("message", (ev: MessageEvent) => { signing.setGrantAllowancesUnchecked(granted); return Promise.resolve(); }, + false, ); break; } case "resetSessionState": void handleSessionActivation(msg.requestId, "resetSessionState", (rt) => - rt.resetSessionState(), + isPairingRuntime(rt) + ? rt.resetSessionState() + : Promise.reject(new Error("pairing runtime is not active")), ); break; + case "activateLocalSessionWithIdentity": { + identityAbort?.abort(new Error("local identity activation changed")); + const { secret, liteUsername } = msg; + void handleSessionActivation( + msg.requestId, + "activateLocalSessionWithIdentity", + (rt) => + isSigningRuntime(rt) + ? rt.activateLocalSessionWithIdentity(secret, liteUsername) + : Promise.reject(new Error("signing runtime is not active")), + ); + break; + } + case "refreshLocalIdentity": + handleLocalIdentity(msg.requestId); + break; + case "registerLocalLiteUsername": + handleLocalIdentity(msg.requestId, msg); + break; + case "getWalletAllowanceSnapshot": + handleWalletAllowanceSnapshot(msg.requestId, msg.productIds); + break; case "getPermissionAuthorizationStatus": void handleGetPermissionAuthorizationStatus( runtime, @@ -964,8 +1131,14 @@ ctx.addEventListener("message", (ev: MessageEvent) => { // down; free the captured handle after the cores finish disposing. const disposing = runtime; runtime = null; + allowanceGeneration++; + identityAbort?.abort(new Error("runtime disposed")); void (async () => { try { + if (disposing && isSigningRuntime(disposing)) + await disposing.disconnectSession(); + await Promise.allSettled(identityOperations); + await Promise.allSettled(allowanceOperations); await Promise.all( [...cores.keys()].map((coreId) => disposeCore(coreId)), ); @@ -1001,7 +1174,8 @@ async function disposeCore(coreId: number): Promise { async function handleSessionActivation( requestId: number, label: string, - activate: (runtime: WorkerPairingHostRuntime) => Promise, + activate: (runtime: WorkerHostRuntime) => Promise, + changesIdentity = true, ): Promise { if (!runtime) { postToMain({ @@ -1012,6 +1186,10 @@ async function handleSessionActivation( }); return; } + if (changesIdentity) { + allowanceGeneration++; + identityAbort?.abort(new Error("local identity activation changed")); + } try { await activate(runtime); postToMain({ kind: "sessionActivationResponse", requestId, ok: true }); @@ -1035,6 +1213,8 @@ async function handleDisconnectSession(requestId: number): Promise { }); return; } + allowanceGeneration++; + identityAbort?.abort(new Error("local identity disconnected")); try { await runtime.disconnectSession(); postToMain({ kind: "disconnectSessionResponse", requestId, ok: true }); diff --git a/js/packages/truapi-host/src/worker-wasm-import.test.ts b/js/packages/truapi-host/src/worker-wasm-import.test.ts index 9c762b798..3211143fc 100644 --- a/js/packages/truapi-host/src/worker-wasm-import.test.ts +++ b/js/packages/truapi-host/src/worker-wasm-import.test.ts @@ -17,6 +17,11 @@ const gluePath = join(packageRoot, "dist", GLUE_SPECIFIER); // Every binding the worker reaches for through `WasmModuleShape`, and that // `src/wasm/web/truapi_server.d.ts` declares. `default` is the wasm-pack init. +// +// `WasmSigningHostRuntime` is deliberately absent here: the production `web` +// bundle is built `--no-default-features`, so it carries no signing host and +// the constructor is optional on `WasmModuleShape`. It is asserted against the +// `testing` bundle below, which is the one built with `wasm-signing-host`. const GLUE_EXPORTS = [ "default", "WasmPairingHostRuntime", @@ -24,6 +29,9 @@ const GLUE_EXPORTS = [ "setLogLevel", ] as const; +const TESTING_GLUE_SPECIFIER = "./wasm/testing/truapi_server.js"; +const testingGluePath = join(packageRoot, "dist", TESTING_GLUE_SPECIFIER); + describe("worker wasm import", () => { it("names the glue in a statically analysable import", () => { expect( @@ -59,4 +67,28 @@ describe("worker wasm import", () => { } }, ); + + // The signing host lives only in the `testing` bundle. Asserting it here + // keeps the split honest in both directions: a rename still fails, and the + // production bundle is not quietly expected to carry a signing host. + it.skipIf(!existsSync(testingGluePath))( + "exposes the signing host in the testing bundle only", + async () => { + const testingGlue: Record = await import( + pathToFileURL(testingGluePath).href + ); + expect( + typeof testingGlue.WasmSigningHostRuntime, + "testing glue must export `WasmSigningHostRuntime`", + ).toBe("function"); + + const webGlue: Record = await import( + pathToFileURL(gluePath).href + ); + expect( + webGlue.WasmSigningHostRuntime, + "production `web` glue must not carry a signing host", + ).toBeUndefined(); + }, + ); }); diff --git a/rust/crates/truapi-client/Cargo.toml b/rust/crates/truapi-client/Cargo.toml new file mode 100644 index 000000000..aa5d4c086 --- /dev/null +++ b/rust/crates/truapi-client/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "truapi-client" +version = "0.16.0" +edition.workspace = true +license.workspace = true +description = "Transport-neutral no-std TrUAPI client codecs and generated method catalog" +publish = false + +[dependencies] +parity-scale-codec = { version = "3", default-features = false } +truapi = { path = "../truapi", default-features = false } + +[lints] +workspace = true diff --git a/rust/crates/truapi-client/src/generated.rs b/rust/crates/truapi-client/src/generated.rs new file mode 100644 index 000000000..be782ade4 --- /dev/null +++ b/rust/crates/truapi-client/src/generated.rs @@ -0,0 +1,2481 @@ +//! Canonical TrUAPI client method catalog. +//! +//! Auto-generated by truapi-codegen. Do not edit. + +use super::*; + +/// Fingerprint of the generated wire contract. +pub const TRUAPI_WIRE_SCHEMA_HASH: &str = "462dacb6e0d1f504"; + +/// `account_connection_status_subscribe` method marker. +pub struct AccountConnectionStatusSubscribe; +impl AccountConnectionStatusSubscribe { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Account", + method: "connection_status_subscribe", + wire_name: "account_connection_status_subscribe", + request_type: "truapi::versioned::account::HostAccountConnectionStatusSubscribeRequest", + response_type: "truapi::versioned::account::HostAccountConnectionStatusSubscribeItem", + error_type: Some("truapi::versioned::account::HostAccountConnectionStatusSubscribeError"), + kind: MethodKind::Subscription, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Subscription(MethodIds { + trait_id: 2, + method_id: 0, + }), + }; +} +impl SubscriptionMethod for AccountConnectionStatusSubscribe { + type Request = truapi::versioned::account::HostAccountConnectionStatusSubscribeRequest; + type Error = truapi::versioned::account::HostAccountConnectionStatusSubscribeError; + type Item = truapi::versioned::account::HostAccountConnectionStatusSubscribeItem; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `account_get_account` method marker. +pub struct AccountGetAccount; +impl AccountGetAccount { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Account", + method: "get_account", + wire_name: "account_get_account", + request_type: "truapi::versioned::account::HostAccountGetRequest", + response_type: "truapi::versioned::account::HostAccountGetResponse", + error_type: Some("truapi::versioned::account::HostAccountGetError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 2, + method_id: 1, + }), + }; +} +impl RequestMethod for AccountGetAccount { + type Request = truapi::versioned::account::HostAccountGetRequest; + type Response = truapi::versioned::account::HostAccountGetResponse; + type Error = truapi::versioned::account::HostAccountGetError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `account_get_account_alias` method marker. +pub struct AccountGetAccountAlias; +impl AccountGetAccountAlias { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Account", + method: "get_account_alias", + wire_name: "account_get_account_alias", + request_type: "truapi::versioned::account::HostAccountGetAliasRequest", + response_type: "truapi::versioned::account::HostAccountGetAliasResponse", + error_type: Some("truapi::versioned::account::HostAccountGetAliasError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 2, + method_id: 2, + }), + }; +} +impl RequestMethod for AccountGetAccountAlias { + type Request = truapi::versioned::account::HostAccountGetAliasRequest; + type Response = truapi::versioned::account::HostAccountGetAliasResponse; + type Error = truapi::versioned::account::HostAccountGetAliasError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `account_create_account_proof` method marker. +pub struct AccountCreateAccountProof; +impl AccountCreateAccountProof { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Account", + method: "create_account_proof", + wire_name: "account_create_account_proof", + request_type: "truapi::versioned::account::HostAccountCreateProofRequest", + response_type: "truapi::versioned::account::HostAccountCreateProofResponse", + error_type: Some("truapi::versioned::account::HostAccountCreateProofError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 2, + method_id: 3, + }), + }; +} +impl RequestMethod for AccountCreateAccountProof { + type Request = truapi::versioned::account::HostAccountCreateProofRequest; + type Response = truapi::versioned::account::HostAccountCreateProofResponse; + type Error = truapi::versioned::account::HostAccountCreateProofError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `account_sign_vrf` method marker. +pub struct AccountSignVrf; +impl AccountSignVrf { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Account", + method: "sign_vrf", + wire_name: "account_sign_vrf", + request_type: "truapi::versioned::account::HostAccountSignVrfRequest", + response_type: "truapi::versioned::account::HostAccountSignVrfResponse", + error_type: Some("truapi::versioned::account::HostAccountSignVrfError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 2, + method_id: 7, + }), + }; +} +impl RequestMethod for AccountSignVrf { + type Request = truapi::versioned::account::HostAccountSignVrfRequest; + type Response = truapi::versioned::account::HostAccountSignVrfResponse; + type Error = truapi::versioned::account::HostAccountSignVrfError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `account_register_ring_vrf_key` method marker. +pub struct AccountRegisterRingVrfKey; +impl AccountRegisterRingVrfKey { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Account", + method: "register_ring_vrf_key", + wire_name: "account_register_ring_vrf_key", + request_type: "truapi::versioned::account::HostAccountRegisterRingVrfKeyRequest", + response_type: "truapi::versioned::account::HostAccountRegisterRingVrfKeyResponse", + error_type: Some("truapi::versioned::account::HostAccountRegisterRingVrfKeyError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 2, + method_id: 8, + }), + }; +} +impl RequestMethod for AccountRegisterRingVrfKey { + type Request = truapi::versioned::account::HostAccountRegisterRingVrfKeyRequest; + type Response = truapi::versioned::account::HostAccountRegisterRingVrfKeyResponse; + type Error = truapi::versioned::account::HostAccountRegisterRingVrfKeyError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `account_list_ring_vrf_keys` method marker. +pub struct AccountListRingVrfKeys; +impl AccountListRingVrfKeys { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Account", + method: "list_ring_vrf_keys", + wire_name: "account_list_ring_vrf_keys", + request_type: "truapi::versioned::account::HostAccountListRingVrfKeysRequest", + response_type: "truapi::versioned::account::HostAccountListRingVrfKeysResponse", + error_type: Some("truapi::versioned::account::HostAccountListRingVrfKeysError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 2, + method_id: 9, + }), + }; +} +impl RequestMethod for AccountListRingVrfKeys { + type Request = truapi::versioned::account::HostAccountListRingVrfKeysRequest; + type Response = truapi::versioned::account::HostAccountListRingVrfKeysResponse; + type Error = truapi::versioned::account::HostAccountListRingVrfKeysError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `account_ring_vrf_sign` method marker. +pub struct AccountRingVrfSign; +impl AccountRingVrfSign { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Account", + method: "ring_vrf_sign", + wire_name: "account_ring_vrf_sign", + request_type: "truapi::versioned::account::HostAccountRingVrfSignRequest", + response_type: "truapi::versioned::account::HostAccountRingVrfSignResponse", + error_type: Some("truapi::versioned::account::HostAccountRingVrfSignError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 2, + method_id: 10, + }), + }; +} +impl RequestMethod for AccountRingVrfSign { + type Request = truapi::versioned::account::HostAccountRingVrfSignRequest; + type Response = truapi::versioned::account::HostAccountRingVrfSignResponse; + type Error = truapi::versioned::account::HostAccountRingVrfSignError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `account_get_legacy_accounts` method marker. +pub struct AccountGetLegacyAccounts; +impl AccountGetLegacyAccounts { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Account", + method: "get_legacy_accounts", + wire_name: "account_get_legacy_accounts", + request_type: "truapi::versioned::account::HostGetLegacyAccountsRequest", + response_type: "truapi::versioned::account::HostGetLegacyAccountsResponse", + error_type: Some("truapi::versioned::account::HostGetLegacyAccountsError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 2, + method_id: 4, + }), + }; +} +impl RequestMethod for AccountGetLegacyAccounts { + type Request = truapi::versioned::account::HostGetLegacyAccountsRequest; + type Response = truapi::versioned::account::HostGetLegacyAccountsResponse; + type Error = truapi::versioned::account::HostGetLegacyAccountsError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `account_get_user_id` method marker. +pub struct AccountGetUserId; +impl AccountGetUserId { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Account", + method: "get_user_id", + wire_name: "account_get_user_id", + request_type: "truapi::versioned::account::HostGetUserIdRequest", + response_type: "truapi::versioned::account::HostGetUserIdResponse", + error_type: Some("truapi::versioned::account::HostGetUserIdError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 2, + method_id: 5, + }), + }; +} +impl RequestMethod for AccountGetUserId { + type Request = truapi::versioned::account::HostGetUserIdRequest; + type Response = truapi::versioned::account::HostGetUserIdResponse; + type Error = truapi::versioned::account::HostGetUserIdError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `account_request_login` method marker. +pub struct AccountRequestLogin; +impl AccountRequestLogin { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Account", + method: "request_login", + wire_name: "account_request_login", + request_type: "truapi::versioned::account::HostRequestLoginRequest", + response_type: "truapi::versioned::account::HostRequestLoginResponse", + error_type: Some("truapi::versioned::account::HostRequestLoginError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 2, + method_id: 6, + }), + }; +} +impl RequestMethod for AccountRequestLogin { + type Request = truapi::versioned::account::HostRequestLoginRequest; + type Response = truapi::versioned::account::HostRequestLoginResponse; + type Error = truapi::versioned::account::HostRequestLoginError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `chain_follow_head_subscribe` method marker. +pub struct ChainFollowHeadSubscribe; +impl ChainFollowHeadSubscribe { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Chain", + method: "follow_head_subscribe", + wire_name: "chain_follow_head_subscribe", + request_type: "truapi::versioned::chain::RemoteChainHeadFollowRequest", + response_type: "truapi::versioned::chain::RemoteChainHeadFollowItem", + error_type: Some("truapi::versioned::chain::RemoteChainHeadFollowError"), + kind: MethodKind::Subscription, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Subscription(MethodIds { + trait_id: 3, + method_id: 0, + }), + }; +} +impl SubscriptionMethod for ChainFollowHeadSubscribe { + type Request = truapi::versioned::chain::RemoteChainHeadFollowRequest; + type Error = truapi::versioned::chain::RemoteChainHeadFollowError; + type Item = truapi::versioned::chain::RemoteChainHeadFollowItem; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `chain_get_head_header` method marker. +pub struct ChainGetHeadHeader; +impl ChainGetHeadHeader { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Chain", + method: "get_head_header", + wire_name: "chain_get_head_header", + request_type: "truapi::versioned::chain::RemoteChainHeadHeaderRequest", + response_type: "truapi::versioned::chain::RemoteChainHeadHeaderResponse", + error_type: Some("truapi::versioned::chain::RemoteChainHeadHeaderError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 3, + method_id: 1, + }), + }; +} +impl RequestMethod for ChainGetHeadHeader { + type Request = truapi::versioned::chain::RemoteChainHeadHeaderRequest; + type Response = truapi::versioned::chain::RemoteChainHeadHeaderResponse; + type Error = truapi::versioned::chain::RemoteChainHeadHeaderError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `chain_get_head_body` method marker. +pub struct ChainGetHeadBody; +impl ChainGetHeadBody { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Chain", + method: "get_head_body", + wire_name: "chain_get_head_body", + request_type: "truapi::versioned::chain::RemoteChainHeadBodyRequest", + response_type: "truapi::versioned::chain::RemoteChainHeadBodyResponse", + error_type: Some("truapi::versioned::chain::RemoteChainHeadBodyError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 3, + method_id: 2, + }), + }; +} +impl RequestMethod for ChainGetHeadBody { + type Request = truapi::versioned::chain::RemoteChainHeadBodyRequest; + type Response = truapi::versioned::chain::RemoteChainHeadBodyResponse; + type Error = truapi::versioned::chain::RemoteChainHeadBodyError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `chain_get_head_storage` method marker. +pub struct ChainGetHeadStorage; +impl ChainGetHeadStorage { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Chain", + method: "get_head_storage", + wire_name: "chain_get_head_storage", + request_type: "truapi::versioned::chain::RemoteChainHeadStorageRequest", + response_type: "truapi::versioned::chain::RemoteChainHeadStorageResponse", + error_type: Some("truapi::versioned::chain::RemoteChainHeadStorageError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 3, + method_id: 3, + }), + }; +} +impl RequestMethod for ChainGetHeadStorage { + type Request = truapi::versioned::chain::RemoteChainHeadStorageRequest; + type Response = truapi::versioned::chain::RemoteChainHeadStorageResponse; + type Error = truapi::versioned::chain::RemoteChainHeadStorageError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `chain_call_head` method marker. +pub struct ChainCallHead; +impl ChainCallHead { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Chain", + method: "call_head", + wire_name: "chain_call_head", + request_type: "truapi::versioned::chain::RemoteChainHeadCallRequest", + response_type: "truapi::versioned::chain::RemoteChainHeadCallResponse", + error_type: Some("truapi::versioned::chain::RemoteChainHeadCallError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 3, + method_id: 4, + }), + }; +} +impl RequestMethod for ChainCallHead { + type Request = truapi::versioned::chain::RemoteChainHeadCallRequest; + type Response = truapi::versioned::chain::RemoteChainHeadCallResponse; + type Error = truapi::versioned::chain::RemoteChainHeadCallError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `chain_unpin_head` method marker. +pub struct ChainUnpinHead; +impl ChainUnpinHead { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Chain", + method: "unpin_head", + wire_name: "chain_unpin_head", + request_type: "truapi::versioned::chain::RemoteChainHeadUnpinRequest", + response_type: "truapi::versioned::chain::RemoteChainHeadUnpinResponse", + error_type: Some("truapi::versioned::chain::RemoteChainHeadUnpinError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 3, + method_id: 5, + }), + }; +} +impl RequestMethod for ChainUnpinHead { + type Request = truapi::versioned::chain::RemoteChainHeadUnpinRequest; + type Response = truapi::versioned::chain::RemoteChainHeadUnpinResponse; + type Error = truapi::versioned::chain::RemoteChainHeadUnpinError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `chain_continue_head` method marker. +pub struct ChainContinueHead; +impl ChainContinueHead { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Chain", + method: "continue_head", + wire_name: "chain_continue_head", + request_type: "truapi::versioned::chain::RemoteChainHeadContinueRequest", + response_type: "truapi::versioned::chain::RemoteChainHeadContinueResponse", + error_type: Some("truapi::versioned::chain::RemoteChainHeadContinueError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 3, + method_id: 6, + }), + }; +} +impl RequestMethod for ChainContinueHead { + type Request = truapi::versioned::chain::RemoteChainHeadContinueRequest; + type Response = truapi::versioned::chain::RemoteChainHeadContinueResponse; + type Error = truapi::versioned::chain::RemoteChainHeadContinueError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `chain_stop_head_operation` method marker. +pub struct ChainStopHeadOperation; +impl ChainStopHeadOperation { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Chain", + method: "stop_head_operation", + wire_name: "chain_stop_head_operation", + request_type: "truapi::versioned::chain::RemoteChainHeadStopOperationRequest", + response_type: "truapi::versioned::chain::RemoteChainHeadStopOperationResponse", + error_type: Some("truapi::versioned::chain::RemoteChainHeadStopOperationError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 3, + method_id: 7, + }), + }; +} +impl RequestMethod for ChainStopHeadOperation { + type Request = truapi::versioned::chain::RemoteChainHeadStopOperationRequest; + type Response = truapi::versioned::chain::RemoteChainHeadStopOperationResponse; + type Error = truapi::versioned::chain::RemoteChainHeadStopOperationError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `chain_get_spec_genesis_hash` method marker. +pub struct ChainGetSpecGenesisHash; +impl ChainGetSpecGenesisHash { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Chain", + method: "get_spec_genesis_hash", + wire_name: "chain_get_spec_genesis_hash", + request_type: "truapi::versioned::chain::RemoteChainSpecGenesisHashRequest", + response_type: "truapi::versioned::chain::RemoteChainSpecGenesisHashResponse", + error_type: Some("truapi::versioned::chain::RemoteChainSpecGenesisHashError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 3, + method_id: 8, + }), + }; +} +impl RequestMethod for ChainGetSpecGenesisHash { + type Request = truapi::versioned::chain::RemoteChainSpecGenesisHashRequest; + type Response = truapi::versioned::chain::RemoteChainSpecGenesisHashResponse; + type Error = truapi::versioned::chain::RemoteChainSpecGenesisHashError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `chain_get_spec_chain_name` method marker. +pub struct ChainGetSpecChainName; +impl ChainGetSpecChainName { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Chain", + method: "get_spec_chain_name", + wire_name: "chain_get_spec_chain_name", + request_type: "truapi::versioned::chain::RemoteChainSpecChainNameRequest", + response_type: "truapi::versioned::chain::RemoteChainSpecChainNameResponse", + error_type: Some("truapi::versioned::chain::RemoteChainSpecChainNameError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 3, + method_id: 9, + }), + }; +} +impl RequestMethod for ChainGetSpecChainName { + type Request = truapi::versioned::chain::RemoteChainSpecChainNameRequest; + type Response = truapi::versioned::chain::RemoteChainSpecChainNameResponse; + type Error = truapi::versioned::chain::RemoteChainSpecChainNameError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `chain_get_spec_properties` method marker. +pub struct ChainGetSpecProperties; +impl ChainGetSpecProperties { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Chain", + method: "get_spec_properties", + wire_name: "chain_get_spec_properties", + request_type: "truapi::versioned::chain::RemoteChainSpecPropertiesRequest", + response_type: "truapi::versioned::chain::RemoteChainSpecPropertiesResponse", + error_type: Some("truapi::versioned::chain::RemoteChainSpecPropertiesError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 3, + method_id: 10, + }), + }; +} +impl RequestMethod for ChainGetSpecProperties { + type Request = truapi::versioned::chain::RemoteChainSpecPropertiesRequest; + type Response = truapi::versioned::chain::RemoteChainSpecPropertiesResponse; + type Error = truapi::versioned::chain::RemoteChainSpecPropertiesError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `chain_broadcast_transaction` method marker. +pub struct ChainBroadcastTransaction; +impl ChainBroadcastTransaction { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Chain", + method: "broadcast_transaction", + wire_name: "chain_broadcast_transaction", + request_type: "truapi::versioned::chain::RemoteChainTransactionBroadcastRequest", + response_type: "truapi::versioned::chain::RemoteChainTransactionBroadcastResponse", + error_type: Some("truapi::versioned::chain::RemoteChainTransactionBroadcastError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 3, + method_id: 11, + }), + }; +} +impl RequestMethod for ChainBroadcastTransaction { + type Request = truapi::versioned::chain::RemoteChainTransactionBroadcastRequest; + type Response = truapi::versioned::chain::RemoteChainTransactionBroadcastResponse; + type Error = truapi::versioned::chain::RemoteChainTransactionBroadcastError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `chain_stop_transaction` method marker. +pub struct ChainStopTransaction; +impl ChainStopTransaction { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Chain", + method: "stop_transaction", + wire_name: "chain_stop_transaction", + request_type: "truapi::versioned::chain::RemoteChainTransactionStopRequest", + response_type: "truapi::versioned::chain::RemoteChainTransactionStopResponse", + error_type: Some("truapi::versioned::chain::RemoteChainTransactionStopError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 3, + method_id: 12, + }), + }; +} +impl RequestMethod for ChainStopTransaction { + type Request = truapi::versioned::chain::RemoteChainTransactionStopRequest; + type Response = truapi::versioned::chain::RemoteChainTransactionStopResponse; + type Error = truapi::versioned::chain::RemoteChainTransactionStopError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `chain_get_chain_info` method marker. +pub struct ChainGetChainInfo; +impl ChainGetChainInfo { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Chain", + method: "get_chain_info", + wire_name: "chain_get_chain_info", + request_type: "truapi::versioned::chain::RemoteChainInfoRequest", + response_type: "truapi::versioned::chain::RemoteChainInfoResponse", + error_type: Some("truapi::versioned::chain::RemoteChainInfoError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 3, + method_id: 13, + }), + }; +} +impl RequestMethod for ChainGetChainInfo { + type Request = truapi::versioned::chain::RemoteChainInfoRequest; + type Response = truapi::versioned::chain::RemoteChainInfoResponse; + type Error = truapi::versioned::chain::RemoteChainInfoError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `chat_create_room` method marker. +pub struct ChatCreateRoom; +impl ChatCreateRoom { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Chat", + method: "create_room", + wire_name: "chat_create_room", + request_type: "truapi::versioned::chat::HostChatCreateRoomRequest", + response_type: "truapi::versioned::chat::HostChatCreateRoomResponse", + error_type: Some("truapi::versioned::chat::HostChatCreateRoomError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: Some(ExecutionKind::Worker), + wire: MethodWire::Request(MethodIds { + trait_id: 4, + method_id: 0, + }), + }; +} +impl RequestMethod for ChatCreateRoom { + type Request = truapi::versioned::chat::HostChatCreateRoomRequest; + type Response = truapi::versioned::chat::HostChatCreateRoomResponse; + type Error = truapi::versioned::chat::HostChatCreateRoomError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `chat_register_bot` method marker. +pub struct ChatRegisterBot; +impl ChatRegisterBot { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Chat", + method: "register_bot", + wire_name: "chat_register_bot", + request_type: "truapi::versioned::chat::HostChatRegisterBotRequest", + response_type: "truapi::versioned::chat::HostChatRegisterBotResponse", + error_type: Some("truapi::versioned::chat::HostChatRegisterBotError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: Some(ExecutionKind::Worker), + wire: MethodWire::Request(MethodIds { + trait_id: 4, + method_id: 1, + }), + }; +} +impl RequestMethod for ChatRegisterBot { + type Request = truapi::versioned::chat::HostChatRegisterBotRequest; + type Response = truapi::versioned::chat::HostChatRegisterBotResponse; + type Error = truapi::versioned::chat::HostChatRegisterBotError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `chat_list_subscribe` method marker. +pub struct ChatListSubscribe; +impl ChatListSubscribe { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Chat", + method: "list_subscribe", + wire_name: "chat_list_subscribe", + request_type: "truapi::versioned::chat::HostChatListSubscribeRequest", + response_type: "truapi::versioned::chat::HostChatListSubscribeItem", + error_type: Some("truapi::versioned::chat::HostChatListSubscribeError"), + kind: MethodKind::Subscription, + direction: Direction::ProductToHost, + required_execution: Some(ExecutionKind::Worker), + wire: MethodWire::Subscription(MethodIds { + trait_id: 4, + method_id: 2, + }), + }; +} +impl SubscriptionMethod for ChatListSubscribe { + type Request = truapi::versioned::chat::HostChatListSubscribeRequest; + type Error = truapi::versioned::chat::HostChatListSubscribeError; + type Item = truapi::versioned::chat::HostChatListSubscribeItem; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `chat_post_message` method marker. +pub struct ChatPostMessage; +impl ChatPostMessage { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Chat", + method: "post_message", + wire_name: "chat_post_message", + request_type: "truapi::versioned::chat::HostChatPostMessageRequest", + response_type: "truapi::versioned::chat::HostChatPostMessageResponse", + error_type: Some("truapi::versioned::chat::HostChatPostMessageError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: Some(ExecutionKind::Worker), + wire: MethodWire::Request(MethodIds { + trait_id: 4, + method_id: 3, + }), + }; +} +impl RequestMethod for ChatPostMessage { + type Request = truapi::versioned::chat::HostChatPostMessageRequest; + type Response = truapi::versioned::chat::HostChatPostMessageResponse; + type Error = truapi::versioned::chat::HostChatPostMessageError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `chat_action_subscribe` method marker. +pub struct ChatActionSubscribe; +impl ChatActionSubscribe { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Chat", + method: "action_subscribe", + wire_name: "chat_action_subscribe", + request_type: "truapi::versioned::chat::HostChatActionSubscribeRequest", + response_type: "truapi::versioned::chat::HostChatActionSubscribeItem", + error_type: Some("truapi::versioned::chat::HostChatActionSubscribeError"), + kind: MethodKind::Subscription, + direction: Direction::ProductToHost, + required_execution: Some(ExecutionKind::Worker), + wire: MethodWire::Subscription(MethodIds { + trait_id: 4, + method_id: 4, + }), + }; +} +impl SubscriptionMethod for ChatActionSubscribe { + type Request = truapi::versioned::chat::HostChatActionSubscribeRequest; + type Error = truapi::versioned::chat::HostChatActionSubscribeError; + type Item = truapi::versioned::chat::HostChatActionSubscribeItem; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `coin_payment_create_purse` method marker. +pub struct CoinPaymentCreatePurse; +impl CoinPaymentCreatePurse { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "CoinPayment", + method: "create_purse", + wire_name: "coin_payment_create_purse", + request_type: "truapi::versioned::coin_payment::HostCoinPaymentCreatePurseRequest", + response_type: "truapi::versioned::coin_payment::HostCoinPaymentCreatePurseResponse", + error_type: Some("truapi::versioned::coin_payment::HostCoinPaymentCreatePurseError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 5, + method_id: 0, + }), + }; +} +impl RequestMethod for CoinPaymentCreatePurse { + type Request = truapi::versioned::coin_payment::HostCoinPaymentCreatePurseRequest; + type Response = truapi::versioned::coin_payment::HostCoinPaymentCreatePurseResponse; + type Error = truapi::versioned::coin_payment::HostCoinPaymentCreatePurseError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `coin_payment_query_purse` method marker. +pub struct CoinPaymentQueryPurse; +impl CoinPaymentQueryPurse { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "CoinPayment", + method: "query_purse", + wire_name: "coin_payment_query_purse", + request_type: "truapi::versioned::coin_payment::HostCoinPaymentQueryPurseRequest", + response_type: "truapi::versioned::coin_payment::HostCoinPaymentQueryPurseResponse", + error_type: Some("truapi::versioned::coin_payment::HostCoinPaymentQueryPurseError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 5, + method_id: 1, + }), + }; +} +impl RequestMethod for CoinPaymentQueryPurse { + type Request = truapi::versioned::coin_payment::HostCoinPaymentQueryPurseRequest; + type Response = truapi::versioned::coin_payment::HostCoinPaymentQueryPurseResponse; + type Error = truapi::versioned::coin_payment::HostCoinPaymentQueryPurseError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `coin_payment_rebalance_purse` method marker. +pub struct CoinPaymentRebalancePurse; +impl CoinPaymentRebalancePurse { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "CoinPayment", + method: "rebalance_purse", + wire_name: "coin_payment_rebalance_purse", + request_type: "truapi::versioned::coin_payment::HostCoinPaymentRebalancePurseRequest", + response_type: "truapi::versioned::coin_payment::HostCoinPaymentRebalancePurseItem", + error_type: Some("truapi::versioned::coin_payment::HostCoinPaymentRebalancePurseError"), + kind: MethodKind::Subscription, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Subscription(MethodIds { + trait_id: 5, + method_id: 2, + }), + }; +} +impl SubscriptionMethod for CoinPaymentRebalancePurse { + type Request = truapi::versioned::coin_payment::HostCoinPaymentRebalancePurseRequest; + type Error = truapi::versioned::coin_payment::HostCoinPaymentRebalancePurseError; + type Item = truapi::versioned::coin_payment::HostCoinPaymentRebalancePurseItem; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `coin_payment_delete_purse` method marker. +pub struct CoinPaymentDeletePurse; +impl CoinPaymentDeletePurse { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "CoinPayment", + method: "delete_purse", + wire_name: "coin_payment_delete_purse", + request_type: "truapi::versioned::coin_payment::HostCoinPaymentDeletePurseRequest", + response_type: "truapi::versioned::coin_payment::HostCoinPaymentDeletePurseItem", + error_type: Some("truapi::versioned::coin_payment::HostCoinPaymentDeletePurseError"), + kind: MethodKind::Subscription, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Subscription(MethodIds { + trait_id: 5, + method_id: 3, + }), + }; +} +impl SubscriptionMethod for CoinPaymentDeletePurse { + type Request = truapi::versioned::coin_payment::HostCoinPaymentDeletePurseRequest; + type Error = truapi::versioned::coin_payment::HostCoinPaymentDeletePurseError; + type Item = truapi::versioned::coin_payment::HostCoinPaymentDeletePurseItem; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `coin_payment_create_receivable` method marker. +pub struct CoinPaymentCreateReceivable; +impl CoinPaymentCreateReceivable { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "CoinPayment", + method: "create_receivable", + wire_name: "coin_payment_create_receivable", + request_type: "truapi::versioned::coin_payment::HostCoinPaymentCreateReceivableRequest", + response_type: "truapi::versioned::coin_payment::HostCoinPaymentCreateReceivableResponse", + error_type: Some("truapi::versioned::coin_payment::HostCoinPaymentCreateReceivableError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 5, + method_id: 4, + }), + }; +} +impl RequestMethod for CoinPaymentCreateReceivable { + type Request = truapi::versioned::coin_payment::HostCoinPaymentCreateReceivableRequest; + type Response = truapi::versioned::coin_payment::HostCoinPaymentCreateReceivableResponse; + type Error = truapi::versioned::coin_payment::HostCoinPaymentCreateReceivableError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `coin_payment_create_cheque` method marker. +pub struct CoinPaymentCreateCheque; +impl CoinPaymentCreateCheque { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "CoinPayment", + method: "create_cheque", + wire_name: "coin_payment_create_cheque", + request_type: "truapi::versioned::coin_payment::HostCoinPaymentCreateChequeRequest", + response_type: "truapi::versioned::coin_payment::HostCoinPaymentCreateChequeResponse", + error_type: Some("truapi::versioned::coin_payment::HostCoinPaymentCreateChequeError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 5, + method_id: 5, + }), + }; +} +impl RequestMethod for CoinPaymentCreateCheque { + type Request = truapi::versioned::coin_payment::HostCoinPaymentCreateChequeRequest; + type Response = truapi::versioned::coin_payment::HostCoinPaymentCreateChequeResponse; + type Error = truapi::versioned::coin_payment::HostCoinPaymentCreateChequeError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `coin_payment_deposit` method marker. +pub struct CoinPaymentDeposit; +impl CoinPaymentDeposit { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "CoinPayment", + method: "deposit", + wire_name: "coin_payment_deposit", + request_type: "truapi::versioned::coin_payment::HostCoinPaymentDepositRequest", + response_type: "truapi::versioned::coin_payment::HostCoinPaymentDepositItem", + error_type: Some("truapi::versioned::coin_payment::HostCoinPaymentDepositError"), + kind: MethodKind::Subscription, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Subscription(MethodIds { + trait_id: 5, + method_id: 6, + }), + }; +} +impl SubscriptionMethod for CoinPaymentDeposit { + type Request = truapi::versioned::coin_payment::HostCoinPaymentDepositRequest; + type Error = truapi::versioned::coin_payment::HostCoinPaymentDepositError; + type Item = truapi::versioned::coin_payment::HostCoinPaymentDepositItem; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `coin_payment_refund` method marker. +pub struct CoinPaymentRefund; +impl CoinPaymentRefund { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "CoinPayment", + method: "refund", + wire_name: "coin_payment_refund", + request_type: "truapi::versioned::coin_payment::HostCoinPaymentRefundRequest", + response_type: "truapi::versioned::coin_payment::HostCoinPaymentRefundItem", + error_type: Some("truapi::versioned::coin_payment::HostCoinPaymentRefundError"), + kind: MethodKind::Subscription, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Subscription(MethodIds { + trait_id: 5, + method_id: 7, + }), + }; +} +impl SubscriptionMethod for CoinPaymentRefund { + type Request = truapi::versioned::coin_payment::HostCoinPaymentRefundRequest; + type Error = truapi::versioned::coin_payment::HostCoinPaymentRefundError; + type Item = truapi::versioned::coin_payment::HostCoinPaymentRefundItem; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `coin_payment_listen_for_payment` method marker. +pub struct CoinPaymentListenForPayment; +impl CoinPaymentListenForPayment { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "CoinPayment", + method: "listen_for_payment", + wire_name: "coin_payment_listen_for_payment", + request_type: "truapi::versioned::coin_payment::HostCoinPaymentListenForRequest", + response_type: "truapi::versioned::coin_payment::HostCoinPaymentListenForItem", + error_type: Some("truapi::versioned::coin_payment::HostCoinPaymentListenForError"), + kind: MethodKind::Subscription, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Subscription(MethodIds { + trait_id: 5, + method_id: 8, + }), + }; +} +impl SubscriptionMethod for CoinPaymentListenForPayment { + type Request = truapi::versioned::coin_payment::HostCoinPaymentListenForRequest; + type Error = truapi::versioned::coin_payment::HostCoinPaymentListenForError; + type Item = truapi::versioned::coin_payment::HostCoinPaymentListenForItem; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `entropy_derive` method marker. +pub struct EntropyDerive; +impl EntropyDerive { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Entropy", + method: "derive", + wire_name: "entropy_derive", + request_type: "truapi::versioned::entropy::HostDeriveEntropyRequest", + response_type: "truapi::versioned::entropy::HostDeriveEntropyResponse", + error_type: Some("truapi::versioned::entropy::HostDeriveEntropyError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 6, + method_id: 0, + }), + }; +} +impl RequestMethod for EntropyDerive { + type Request = truapi::versioned::entropy::HostDeriveEntropyRequest; + type Response = truapi::versioned::entropy::HostDeriveEntropyResponse; + type Error = truapi::versioned::entropy::HostDeriveEntropyError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `local_storage_read` method marker. +pub struct LocalStorageRead; +impl LocalStorageRead { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "LocalStorage", + method: "read", + wire_name: "local_storage_read", + request_type: "truapi::versioned::local_storage::HostLocalStorageReadRequest", + response_type: "truapi::versioned::local_storage::HostLocalStorageReadResponse", + error_type: Some("truapi::versioned::local_storage::HostLocalStorageReadError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 7, + method_id: 0, + }), + }; +} +impl RequestMethod for LocalStorageRead { + type Request = truapi::versioned::local_storage::HostLocalStorageReadRequest; + type Response = truapi::versioned::local_storage::HostLocalStorageReadResponse; + type Error = truapi::versioned::local_storage::HostLocalStorageReadError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `local_storage_write` method marker. +pub struct LocalStorageWrite; +impl LocalStorageWrite { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "LocalStorage", + method: "write", + wire_name: "local_storage_write", + request_type: "truapi::versioned::local_storage::HostLocalStorageWriteRequest", + response_type: "truapi::versioned::local_storage::HostLocalStorageWriteResponse", + error_type: Some("truapi::versioned::local_storage::HostLocalStorageWriteError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 7, + method_id: 1, + }), + }; +} +impl RequestMethod for LocalStorageWrite { + type Request = truapi::versioned::local_storage::HostLocalStorageWriteRequest; + type Response = truapi::versioned::local_storage::HostLocalStorageWriteResponse; + type Error = truapi::versioned::local_storage::HostLocalStorageWriteError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `local_storage_clear` method marker. +pub struct LocalStorageClear; +impl LocalStorageClear { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "LocalStorage", + method: "clear", + wire_name: "local_storage_clear", + request_type: "truapi::versioned::local_storage::HostLocalStorageClearRequest", + response_type: "truapi::versioned::local_storage::HostLocalStorageClearResponse", + error_type: Some("truapi::versioned::local_storage::HostLocalStorageClearError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 7, + method_id: 2, + }), + }; +} +impl RequestMethod for LocalStorageClear { + type Request = truapi::versioned::local_storage::HostLocalStorageClearRequest; + type Response = truapi::versioned::local_storage::HostLocalStorageClearResponse; + type Error = truapi::versioned::local_storage::HostLocalStorageClearError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `local_storage_subscribe` method marker. +pub struct LocalStorageSubscribe; +impl LocalStorageSubscribe { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "LocalStorage", + method: "subscribe", + wire_name: "local_storage_subscribe", + request_type: "truapi::versioned::local_storage::HostLocalStorageSubscribeRequest", + response_type: "truapi::versioned::local_storage::HostLocalStorageChangeItem", + error_type: Some("truapi::versioned::local_storage::HostLocalStorageSubscribeError"), + kind: MethodKind::Subscription, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Subscription(MethodIds { + trait_id: 7, + method_id: 3, + }), + }; +} +impl SubscriptionMethod for LocalStorageSubscribe { + type Request = truapi::versioned::local_storage::HostLocalStorageSubscribeRequest; + type Error = truapi::versioned::local_storage::HostLocalStorageSubscribeError; + type Item = truapi::versioned::local_storage::HostLocalStorageChangeItem; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `locale_subscribe` method marker. +pub struct LocaleSubscribe; +impl LocaleSubscribe { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Locale", + method: "subscribe", + wire_name: "locale_subscribe", + request_type: "truapi::versioned::locale::HostLocaleSubscribeRequest", + response_type: "truapi::versioned::locale::HostLocaleSubscribeItem", + error_type: Some("truapi::versioned::locale::HostLocaleSubscribeError"), + kind: MethodKind::Subscription, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Subscription(MethodIds { + trait_id: 16, + method_id: 0, + }), + }; +} +impl SubscriptionMethod for LocaleSubscribe { + type Request = truapi::versioned::locale::HostLocaleSubscribeRequest; + type Error = truapi::versioned::locale::HostLocaleSubscribeError; + type Item = truapi::versioned::locale::HostLocaleSubscribeItem; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `notifications_send_push_notification` method marker. +pub struct NotificationsSendPushNotification; +impl NotificationsSendPushNotification { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Notifications", + method: "send_push_notification", + wire_name: "notifications_send_push_notification", + request_type: "truapi::versioned::notifications::HostPushNotificationRequest", + response_type: "truapi::versioned::notifications::HostPushNotificationResponse", + error_type: Some("truapi::versioned::notifications::HostPushNotificationError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 8, + method_id: 0, + }), + }; +} +impl RequestMethod for NotificationsSendPushNotification { + type Request = truapi::versioned::notifications::HostPushNotificationRequest; + type Response = truapi::versioned::notifications::HostPushNotificationResponse; + type Error = truapi::versioned::notifications::HostPushNotificationError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `notifications_cancel_push_notification` method marker. +pub struct NotificationsCancelPushNotification; +impl NotificationsCancelPushNotification { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Notifications", + method: "cancel_push_notification", + wire_name: "notifications_cancel_push_notification", + request_type: "truapi::versioned::notifications::HostPushNotificationCancelRequest", + response_type: "truapi::versioned::notifications::HostPushNotificationCancelResponse", + error_type: Some("truapi::versioned::notifications::HostPushNotificationCancelError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 8, + method_id: 1, + }), + }; +} +impl RequestMethod for NotificationsCancelPushNotification { + type Request = truapi::versioned::notifications::HostPushNotificationCancelRequest; + type Response = truapi::versioned::notifications::HostPushNotificationCancelResponse; + type Error = truapi::versioned::notifications::HostPushNotificationCancelError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `payment_balance_subscribe` method marker. +pub struct PaymentBalanceSubscribe; +impl PaymentBalanceSubscribe { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Payment", + method: "balance_subscribe", + wire_name: "payment_balance_subscribe", + request_type: "truapi::versioned::payment::HostPaymentBalanceSubscribeRequest", + response_type: "truapi::versioned::payment::HostPaymentBalanceSubscribeItem", + error_type: Some("truapi::versioned::payment::HostPaymentBalanceSubscribeError"), + kind: MethodKind::Subscription, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Subscription(MethodIds { + trait_id: 9, + method_id: 0, + }), + }; +} +impl SubscriptionMethod for PaymentBalanceSubscribe { + type Request = truapi::versioned::payment::HostPaymentBalanceSubscribeRequest; + type Error = truapi::versioned::payment::HostPaymentBalanceSubscribeError; + type Item = truapi::versioned::payment::HostPaymentBalanceSubscribeItem; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `payment_request` method marker. +pub struct PaymentRequest; +impl PaymentRequest { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Payment", + method: "request", + wire_name: "payment_request", + request_type: "truapi::versioned::payment::HostPaymentRequest", + response_type: "truapi::versioned::payment::HostPaymentResponse", + error_type: Some("truapi::versioned::payment::HostPaymentError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 9, + method_id: 2, + }), + }; +} +impl RequestMethod for PaymentRequest { + type Request = truapi::versioned::payment::HostPaymentRequest; + type Response = truapi::versioned::payment::HostPaymentResponse; + type Error = truapi::versioned::payment::HostPaymentError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `payment_status_subscribe` method marker. +pub struct PaymentStatusSubscribe; +impl PaymentStatusSubscribe { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Payment", + method: "status_subscribe", + wire_name: "payment_status_subscribe", + request_type: "truapi::versioned::payment::HostPaymentStatusSubscribeRequest", + response_type: "truapi::versioned::payment::HostPaymentStatusSubscribeItem", + error_type: Some("truapi::versioned::payment::HostPaymentStatusSubscribeError"), + kind: MethodKind::Subscription, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Subscription(MethodIds { + trait_id: 9, + method_id: 3, + }), + }; +} +impl SubscriptionMethod for PaymentStatusSubscribe { + type Request = truapi::versioned::payment::HostPaymentStatusSubscribeRequest; + type Error = truapi::versioned::payment::HostPaymentStatusSubscribeError; + type Item = truapi::versioned::payment::HostPaymentStatusSubscribeItem; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `payment_top_up` method marker. +pub struct PaymentTopUp; +impl PaymentTopUp { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Payment", + method: "top_up", + wire_name: "payment_top_up", + request_type: "truapi::versioned::payment::HostPaymentTopUpRequest", + response_type: "truapi::versioned::payment::HostPaymentTopUpResponse", + error_type: Some("truapi::versioned::payment::HostPaymentTopUpError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 9, + method_id: 1, + }), + }; +} +impl RequestMethod for PaymentTopUp { + type Request = truapi::versioned::payment::HostPaymentTopUpRequest; + type Response = truapi::versioned::payment::HostPaymentTopUpResponse; + type Error = truapi::versioned::payment::HostPaymentTopUpError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `permissions_request_device_permission` method marker. +pub struct PermissionsRequestDevicePermission; +impl PermissionsRequestDevicePermission { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Permissions", + method: "request_device_permission", + wire_name: "permissions_request_device_permission", + request_type: "truapi::versioned::permissions::HostDevicePermissionRequest", + response_type: "truapi::versioned::permissions::HostDevicePermissionResponse", + error_type: Some("truapi::versioned::permissions::HostDevicePermissionError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 10, + method_id: 0, + }), + }; +} +impl RequestMethod for PermissionsRequestDevicePermission { + type Request = truapi::versioned::permissions::HostDevicePermissionRequest; + type Response = truapi::versioned::permissions::HostDevicePermissionResponse; + type Error = truapi::versioned::permissions::HostDevicePermissionError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `permissions_request_remote_permission` method marker. +pub struct PermissionsRequestRemotePermission; +impl PermissionsRequestRemotePermission { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Permissions", + method: "request_remote_permission", + wire_name: "permissions_request_remote_permission", + request_type: "truapi::versioned::permissions::RemotePermissionRequest", + response_type: "truapi::versioned::permissions::RemotePermissionResponse", + error_type: Some("truapi::versioned::permissions::RemotePermissionError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 10, + method_id: 1, + }), + }; +} +impl RequestMethod for PermissionsRequestRemotePermission { + type Request = truapi::versioned::permissions::RemotePermissionRequest; + type Response = truapi::versioned::permissions::RemotePermissionResponse; + type Error = truapi::versioned::permissions::RemotePermissionError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `permissions_authorize_remote_permission` method marker. +pub struct PermissionsAuthorizeRemotePermission; +impl PermissionsAuthorizeRemotePermission { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Permissions", + method: "authorize_remote_permission", + wire_name: "permissions_authorize_remote_permission", + request_type: "truapi::versioned::permissions::RemotePermissionRequest", + response_type: "truapi::versioned::permissions::RemotePermissionResponse", + error_type: Some("truapi::versioned::permissions::RemotePermissionError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 10, + method_id: 2, + }), + }; +} +impl RequestMethod for PermissionsAuthorizeRemotePermission { + type Request = truapi::versioned::permissions::RemotePermissionRequest; + type Response = truapi::versioned::permissions::RemotePermissionResponse; + type Error = truapi::versioned::permissions::RemotePermissionError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `permissions_authorize_device_permission` method marker. +pub struct PermissionsAuthorizeDevicePermission; +impl PermissionsAuthorizeDevicePermission { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Permissions", + method: "authorize_device_permission", + wire_name: "permissions_authorize_device_permission", + request_type: "truapi::versioned::permissions::HostDevicePermissionRequest", + response_type: "truapi::versioned::permissions::HostDevicePermissionResponse", + error_type: Some("truapi::versioned::permissions::HostDevicePermissionError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 10, + method_id: 3, + }), + }; +} +impl RequestMethod for PermissionsAuthorizeDevicePermission { + type Request = truapi::versioned::permissions::HostDevicePermissionRequest; + type Response = truapi::versioned::permissions::HostDevicePermissionResponse; + type Error = truapi::versioned::permissions::HostDevicePermissionError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `pocket_list_subscribe` method marker. +pub struct PocketListSubscribe; +impl PocketListSubscribe { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Pocket", + method: "list_subscribe", + wire_name: "pocket_list_subscribe", + request_type: "truapi::versioned::pocket::HostPocketListSubscribeRequest", + response_type: "truapi::versioned::pocket::HostPocketListSubscribeItem", + error_type: Some("truapi::versioned::pocket::HostPocketListSubscribeError"), + kind: MethodKind::Subscription, + direction: Direction::ProductToHost, + required_execution: Some(ExecutionKind::Worker), + wire: MethodWire::Subscription(MethodIds { + trait_id: 18, + method_id: 0, + }), + }; +} +impl SubscriptionMethod for PocketListSubscribe { + type Request = truapi::versioned::pocket::HostPocketListSubscribeRequest; + type Error = truapi::versioned::pocket::HostPocketListSubscribeError; + type Item = truapi::versioned::pocket::HostPocketListSubscribeItem; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `pocket_remove_card` method marker. +pub struct PocketRemoveCard; +impl PocketRemoveCard { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Pocket", + method: "remove_card", + wire_name: "pocket_remove_card", + request_type: "truapi::versioned::pocket::HostPocketRemoveCardRequest", + response_type: "truapi::versioned::pocket::HostPocketRemoveCardResponse", + error_type: Some("truapi::versioned::pocket::HostPocketRemoveCardError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: Some(ExecutionKind::Worker), + wire: MethodWire::Request(MethodIds { + trait_id: 18, + method_id: 1, + }), + }; +} +impl RequestMethod for PocketRemoveCard { + type Request = truapi::versioned::pocket::HostPocketRemoveCardRequest; + type Response = truapi::versioned::pocket::HostPocketRemoveCardResponse; + type Error = truapi::versioned::pocket::HostPocketRemoveCardError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `preimage_lookup_subscribe` method marker. +pub struct PreimageLookupSubscribe; +impl PreimageLookupSubscribe { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Preimage", + method: "lookup_subscribe", + wire_name: "preimage_lookup_subscribe", + request_type: "truapi::versioned::preimage::RemotePreimageLookupSubscribeRequest", + response_type: "truapi::versioned::preimage::RemotePreimageLookupSubscribeItem", + error_type: Some("truapi::versioned::preimage::RemotePreimageLookupSubscribeError"), + kind: MethodKind::Subscription, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Subscription(MethodIds { + trait_id: 11, + method_id: 0, + }), + }; +} +impl SubscriptionMethod for PreimageLookupSubscribe { + type Request = truapi::versioned::preimage::RemotePreimageLookupSubscribeRequest; + type Error = truapi::versioned::preimage::RemotePreimageLookupSubscribeError; + type Item = truapi::versioned::preimage::RemotePreimageLookupSubscribeItem; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `preimage_submit` method marker. +pub struct PreimageSubmit; +impl PreimageSubmit { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Preimage", + method: "submit", + wire_name: "preimage_submit", + request_type: "truapi::versioned::preimage::RemotePreimageSubmitRequest", + response_type: "truapi::versioned::preimage::RemotePreimageSubmitResponse", + error_type: Some("truapi::versioned::preimage::RemotePreimageSubmitError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 11, + method_id: 1, + }), + }; +} +impl RequestMethod for PreimageSubmit { + type Request = truapi::versioned::preimage::RemotePreimageSubmitRequest; + type Response = truapi::versioned::preimage::RemotePreimageSubmitResponse; + type Error = truapi::versioned::preimage::RemotePreimageSubmitError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `renderer_render` method marker. +pub struct RendererRender; +impl RendererRender { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Renderer", + method: "render", + wire_name: "renderer_render", + request_type: "truapi::versioned::renderer::ProductRendererRenderRequest", + response_type: "truapi::versioned::renderer::ProductRendererRenderItem", + error_type: Some("truapi::versioned::renderer::ProductRendererRenderError"), + kind: MethodKind::Subscription, + direction: Direction::HostToProduct, + required_execution: Some(ExecutionKind::Worker), + wire: MethodWire::Subscription(MethodIds { + trait_id: 17, + method_id: 0, + }), + }; +} +impl HostSubscriptionMethod for RendererRender { + type Request = truapi::versioned::renderer::ProductRendererRenderRequest; + type Error = truapi::versioned::renderer::ProductRendererRenderError; + type Item = truapi::versioned::renderer::ProductRendererRenderItem; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `renderer_action_subscribe` method marker. +pub struct RendererActionSubscribe; +impl RendererActionSubscribe { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Renderer", + method: "action_subscribe", + wire_name: "renderer_action_subscribe", + request_type: "truapi::versioned::renderer::HostRendererActionSubscribeRequest", + response_type: "truapi::versioned::renderer::HostRendererActionSubscribeItem", + error_type: Some("truapi::versioned::renderer::HostRendererActionSubscribeError"), + kind: MethodKind::Subscription, + direction: Direction::ProductToHost, + required_execution: Some(ExecutionKind::Worker), + wire: MethodWire::Subscription(MethodIds { + trait_id: 17, + method_id: 1, + }), + }; +} +impl SubscriptionMethod for RendererActionSubscribe { + type Request = truapi::versioned::renderer::HostRendererActionSubscribeRequest; + type Error = truapi::versioned::renderer::HostRendererActionSubscribeError; + type Item = truapi::versioned::renderer::HostRendererActionSubscribeItem; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `resource_allocation_request` method marker. +pub struct ResourceAllocationRequest; +impl ResourceAllocationRequest { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "ResourceAllocation", + method: "request", + wire_name: "resource_allocation_request", + request_type: "truapi::versioned::resource_allocation::HostRequestResourceAllocationRequest", + response_type: "truapi::versioned::resource_allocation::HostRequestResourceAllocationResponse", + error_type: Some( + "truapi::versioned::resource_allocation::HostRequestResourceAllocationError", + ), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 12, + method_id: 0, + }), + }; +} +impl RequestMethod for ResourceAllocationRequest { + type Request = truapi::versioned::resource_allocation::HostRequestResourceAllocationRequest; + type Response = truapi::versioned::resource_allocation::HostRequestResourceAllocationResponse; + type Error = truapi::versioned::resource_allocation::HostRequestResourceAllocationError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `signing_create_transaction` method marker. +pub struct SigningCreateTransaction; +impl SigningCreateTransaction { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Signing", + method: "create_transaction", + wire_name: "signing_create_transaction", + request_type: "truapi::versioned::signing::HostCreateTransactionRequest", + response_type: "truapi::versioned::signing::HostCreateTransactionResponse", + error_type: Some("truapi::versioned::signing::HostCreateTransactionError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 13, + method_id: 0, + }), + }; +} +impl RequestMethod for SigningCreateTransaction { + type Request = truapi::versioned::signing::HostCreateTransactionRequest; + type Response = truapi::versioned::signing::HostCreateTransactionResponse; + type Error = truapi::versioned::signing::HostCreateTransactionError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `signing_create_transaction_with_legacy_account` method marker. +pub struct SigningCreateTransactionWithLegacyAccount; +impl SigningCreateTransactionWithLegacyAccount { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Signing", + method: "create_transaction_with_legacy_account", + wire_name: "signing_create_transaction_with_legacy_account", + request_type: "truapi::versioned::signing::HostCreateTransactionWithLegacyAccountRequest", + response_type: "truapi::versioned::signing::HostCreateTransactionWithLegacyAccountResponse", + error_type: Some("truapi::versioned::signing::HostCreateTransactionWithLegacyAccountError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 13, + method_id: 1, + }), + }; +} +impl RequestMethod for SigningCreateTransactionWithLegacyAccount { + type Request = truapi::versioned::signing::HostCreateTransactionWithLegacyAccountRequest; + type Response = truapi::versioned::signing::HostCreateTransactionWithLegacyAccountResponse; + type Error = truapi::versioned::signing::HostCreateTransactionWithLegacyAccountError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `signing_sign_raw_with_legacy_account` method marker. +pub struct SigningSignRawWithLegacyAccount; +impl SigningSignRawWithLegacyAccount { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Signing", + method: "sign_raw_with_legacy_account", + wire_name: "signing_sign_raw_with_legacy_account", + request_type: "truapi::versioned::signing::HostSignRawWithLegacyAccountRequest", + response_type: "truapi::versioned::signing::HostSignRawWithLegacyAccountResponse", + error_type: Some("truapi::versioned::signing::HostSignRawWithLegacyAccountError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 13, + method_id: 2, + }), + }; +} +impl RequestMethod for SigningSignRawWithLegacyAccount { + type Request = truapi::versioned::signing::HostSignRawWithLegacyAccountRequest; + type Response = truapi::versioned::signing::HostSignRawWithLegacyAccountResponse; + type Error = truapi::versioned::signing::HostSignRawWithLegacyAccountError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `signing_sign_payload_with_legacy_account` method marker. +pub struct SigningSignPayloadWithLegacyAccount; +impl SigningSignPayloadWithLegacyAccount { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Signing", + method: "sign_payload_with_legacy_account", + wire_name: "signing_sign_payload_with_legacy_account", + request_type: "truapi::versioned::signing::HostSignPayloadWithLegacyAccountRequest", + response_type: "truapi::versioned::signing::HostSignPayloadWithLegacyAccountResponse", + error_type: Some("truapi::versioned::signing::HostSignPayloadWithLegacyAccountError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 13, + method_id: 3, + }), + }; +} +impl RequestMethod for SigningSignPayloadWithLegacyAccount { + type Request = truapi::versioned::signing::HostSignPayloadWithLegacyAccountRequest; + type Response = truapi::versioned::signing::HostSignPayloadWithLegacyAccountResponse; + type Error = truapi::versioned::signing::HostSignPayloadWithLegacyAccountError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `signing_sign_raw` method marker. +pub struct SigningSignRaw; +impl SigningSignRaw { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Signing", + method: "sign_raw", + wire_name: "signing_sign_raw", + request_type: "truapi::versioned::signing::HostSignRawRequest", + response_type: "truapi::versioned::signing::HostSignRawResponse", + error_type: Some("truapi::versioned::signing::HostSignRawError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 13, + method_id: 4, + }), + }; +} +impl RequestMethod for SigningSignRaw { + type Request = truapi::versioned::signing::HostSignRawRequest; + type Response = truapi::versioned::signing::HostSignRawResponse; + type Error = truapi::versioned::signing::HostSignRawError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `signing_sign_payload` method marker. +pub struct SigningSignPayload; +impl SigningSignPayload { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Signing", + method: "sign_payload", + wire_name: "signing_sign_payload", + request_type: "truapi::versioned::signing::HostSignPayloadRequest", + response_type: "truapi::versioned::signing::HostSignPayloadResponse", + error_type: Some("truapi::versioned::signing::HostSignPayloadError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 13, + method_id: 5, + }), + }; +} +impl RequestMethod for SigningSignPayload { + type Request = truapi::versioned::signing::HostSignPayloadRequest; + type Response = truapi::versioned::signing::HostSignPayloadResponse; + type Error = truapi::versioned::signing::HostSignPayloadError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `signing_sign_raw_unwatermarked_deprecated` method marker. +pub struct SigningSignRawUnwatermarkedDeprecated; +impl SigningSignRawUnwatermarkedDeprecated { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Signing", + method: "sign_raw_unwatermarked_deprecated", + wire_name: "signing_sign_raw_unwatermarked_deprecated", + request_type: "truapi::versioned::signing::HostSignRawRequest", + response_type: "truapi::versioned::signing::HostSignRawResponse", + error_type: Some("truapi::versioned::signing::HostSignRawError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 13, + method_id: 6, + }), + }; +} +impl RequestMethod for SigningSignRawUnwatermarkedDeprecated { + type Request = truapi::versioned::signing::HostSignRawRequest; + type Response = truapi::versioned::signing::HostSignRawResponse; + type Error = truapi::versioned::signing::HostSignRawError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `signing_sign_raw_unwatermarked_deprecated_with_legacy_account` method marker. +pub struct SigningSignRawUnwatermarkedDeprecatedWithLegacyAccount; +impl SigningSignRawUnwatermarkedDeprecatedWithLegacyAccount { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Signing", + method: "sign_raw_unwatermarked_deprecated_with_legacy_account", + wire_name: "signing_sign_raw_unwatermarked_deprecated_with_legacy_account", + request_type: "truapi::versioned::signing::HostSignRawWithLegacyAccountRequest", + response_type: "truapi::versioned::signing::HostSignRawWithLegacyAccountResponse", + error_type: Some("truapi::versioned::signing::HostSignRawWithLegacyAccountError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 13, + method_id: 7, + }), + }; +} +impl RequestMethod for SigningSignRawUnwatermarkedDeprecatedWithLegacyAccount { + type Request = truapi::versioned::signing::HostSignRawWithLegacyAccountRequest; + type Response = truapi::versioned::signing::HostSignRawWithLegacyAccountResponse; + type Error = truapi::versioned::signing::HostSignRawWithLegacyAccountError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `statement_store_subscribe` method marker. +pub struct StatementStoreSubscribe; +impl StatementStoreSubscribe { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "StatementStore", + method: "subscribe", + wire_name: "statement_store_subscribe", + request_type: "truapi::versioned::statement_store::RemoteStatementStoreSubscribeRequest", + response_type: "truapi::versioned::statement_store::RemoteStatementStoreSubscribeItem", + error_type: Some("truapi::versioned::statement_store::RemoteStatementStoreSubscribeError"), + kind: MethodKind::Subscription, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Subscription(MethodIds { + trait_id: 14, + method_id: 0, + }), + }; +} +impl SubscriptionMethod for StatementStoreSubscribe { + type Request = truapi::versioned::statement_store::RemoteStatementStoreSubscribeRequest; + type Error = truapi::versioned::statement_store::RemoteStatementStoreSubscribeError; + type Item = truapi::versioned::statement_store::RemoteStatementStoreSubscribeItem; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `statement_store_create_proof` method marker. +pub struct StatementStoreCreateProof; +impl StatementStoreCreateProof { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "StatementStore", + method: "create_proof", + wire_name: "statement_store_create_proof", + request_type: "truapi::versioned::statement_store::RemoteStatementStoreCreateProofRequest", + response_type: "truapi::versioned::statement_store::RemoteStatementStoreCreateProofResponse", + error_type: Some( + "truapi::versioned::statement_store::RemoteStatementStoreCreateProofError", + ), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 14, + method_id: 1, + }), + }; +} +impl RequestMethod for StatementStoreCreateProof { + type Request = truapi::versioned::statement_store::RemoteStatementStoreCreateProofRequest; + type Response = truapi::versioned::statement_store::RemoteStatementStoreCreateProofResponse; + type Error = truapi::versioned::statement_store::RemoteStatementStoreCreateProofError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `statement_store_create_proof_authorized` method marker. +pub struct StatementStoreCreateProofAuthorized; +impl StatementStoreCreateProofAuthorized { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "StatementStore", + method: "create_proof_authorized", + wire_name: "statement_store_create_proof_authorized", + request_type: "truapi::versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedRequest", + response_type: "truapi::versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedResponse", + error_type: Some( + "truapi::versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedError", + ), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 14, + method_id: 3, + }), + }; +} +impl RequestMethod for StatementStoreCreateProofAuthorized { + type Request = + truapi::versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedRequest; + type Response = + truapi::versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedResponse; + type Error = truapi::versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `statement_store_submit` method marker. +pub struct StatementStoreSubmit; +impl StatementStoreSubmit { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "StatementStore", + method: "submit", + wire_name: "statement_store_submit", + request_type: "truapi::versioned::statement_store::RemoteStatementStoreSubmitRequest", + response_type: "truapi::versioned::statement_store::RemoteStatementStoreSubmitResponse", + error_type: Some("truapi::versioned::statement_store::RemoteStatementStoreSubmitError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 14, + method_id: 2, + }), + }; +} +impl RequestMethod for StatementStoreSubmit { + type Request = truapi::versioned::statement_store::RemoteStatementStoreSubmitRequest; + type Response = truapi::versioned::statement_store::RemoteStatementStoreSubmitResponse; + type Error = truapi::versioned::statement_store::RemoteStatementStoreSubmitError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `system_handshake` method marker. +pub struct SystemHandshake; +impl SystemHandshake { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "System", + method: "handshake", + wire_name: "system_handshake", + request_type: "truapi::versioned::system::HostHandshakeRequest", + response_type: "truapi::versioned::system::HostHandshakeResponse", + error_type: Some("truapi::versioned::system::HostHandshakeError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 1, + method_id: 0, + }), + }; +} +impl RequestMethod for SystemHandshake { + type Request = truapi::versioned::system::HostHandshakeRequest; + type Response = truapi::versioned::system::HostHandshakeResponse; + type Error = truapi::versioned::system::HostHandshakeError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `system_feature_supported` method marker. +pub struct SystemFeatureSupported; +impl SystemFeatureSupported { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "System", + method: "feature_supported", + wire_name: "system_feature_supported", + request_type: "truapi::versioned::system::HostFeatureSupportedRequest", + response_type: "truapi::versioned::system::HostFeatureSupportedResponse", + error_type: Some("truapi::versioned::system::HostFeatureSupportedError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 1, + method_id: 1, + }), + }; +} +impl RequestMethod for SystemFeatureSupported { + type Request = truapi::versioned::system::HostFeatureSupportedRequest; + type Response = truapi::versioned::system::HostFeatureSupportedResponse; + type Error = truapi::versioned::system::HostFeatureSupportedError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `system_navigate_to` method marker. +pub struct SystemNavigateTo; +impl SystemNavigateTo { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "System", + method: "navigate_to", + wire_name: "system_navigate_to", + request_type: "truapi::versioned::system::HostNavigateToRequest", + response_type: "truapi::versioned::system::HostNavigateToResponse", + error_type: Some("truapi::versioned::system::HostNavigateToError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 1, + method_id: 2, + }), + }; +} +impl RequestMethod for SystemNavigateTo { + type Request = truapi::versioned::system::HostNavigateToRequest; + type Response = truapi::versioned::system::HostNavigateToResponse; + type Error = truapi::versioned::system::HostNavigateToError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `system_host_info` method marker. +pub struct SystemHostInfo; +impl SystemHostInfo { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "System", + method: "host_info", + wire_name: "system_host_info", + request_type: "truapi::versioned::system::HostInfoRequest", + response_type: "truapi::versioned::system::HostInfoResponse", + error_type: Some("truapi::versioned::system::HostInfoError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 1, + method_id: 3, + }), + }; +} +impl RequestMethod for SystemHostInfo { + type Request = truapi::versioned::system::HostInfoRequest; + type Response = truapi::versioned::system::HostInfoResponse; + type Error = truapi::versioned::system::HostInfoError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `system_get_product_context` method marker. +pub struct SystemGetProductContext; +impl SystemGetProductContext { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "System", + method: "get_product_context", + wire_name: "system_get_product_context", + request_type: "truapi::versioned::system::HostGetProductContextRequest", + response_type: "truapi::versioned::system::HostGetProductContextResponse", + error_type: Some("truapi::versioned::system::HostGetProductContextError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Request(MethodIds { + trait_id: 1, + method_id: 4, + }), + }; +} +impl RequestMethod for SystemGetProductContext { + type Request = truapi::versioned::system::HostGetProductContextRequest; + type Response = truapi::versioned::system::HostGetProductContextResponse; + type Error = truapi::versioned::system::HostGetProductContextError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `theme_subscribe` method marker. +pub struct ThemeSubscribe; +impl ThemeSubscribe { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Theme", + method: "subscribe", + wire_name: "theme_subscribe", + request_type: "truapi::versioned::theme::HostThemeSubscribeRequest", + response_type: "truapi::versioned::theme::HostThemeSubscribeItem", + error_type: Some("truapi::versioned::theme::HostThemeSubscribeError"), + kind: MethodKind::Subscription, + direction: Direction::ProductToHost, + required_execution: None, + wire: MethodWire::Subscription(MethodIds { + trait_id: 15, + method_id: 0, + }), + }; +} +impl SubscriptionMethod for ThemeSubscribe { + type Request = truapi::versioned::theme::HostThemeSubscribeRequest; + type Error = truapi::versioned::theme::HostThemeSubscribeError; + type Item = truapi::versioned::theme::HostThemeSubscribeItem; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `worker_begin_operation` method marker. +pub struct WorkerBeginOperation; +impl WorkerBeginOperation { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Worker", + method: "begin_operation", + wire_name: "worker_begin_operation", + request_type: "truapi::versioned::worker::HostWorkerBeginOperationRequest", + response_type: "truapi::versioned::worker::HostWorkerBeginOperationResponse", + error_type: Some("truapi::versioned::worker::HostWorkerBeginOperationError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: Some(ExecutionKind::Worker), + wire: MethodWire::Request(MethodIds { + trait_id: 19, + method_id: 0, + }), + }; +} +impl RequestMethod for WorkerBeginOperation { + type Request = truapi::versioned::worker::HostWorkerBeginOperationRequest; + type Response = truapi::versioned::worker::HostWorkerBeginOperationResponse; + type Error = truapi::versioned::worker::HostWorkerBeginOperationError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// `worker_end_operation` method marker. +pub struct WorkerEndOperation; +impl WorkerEndOperation { + /// Canonical metadata and frame ids for this method. + pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor { + service: "Worker", + method: "end_operation", + wire_name: "worker_end_operation", + request_type: "truapi::versioned::worker::HostWorkerEndOperationRequest", + response_type: "truapi::versioned::worker::HostWorkerEndOperationResponse", + error_type: Some("truapi::versioned::worker::HostWorkerEndOperationError"), + kind: MethodKind::Request, + direction: Direction::ProductToHost, + required_execution: Some(ExecutionKind::Worker), + wire: MethodWire::Request(MethodIds { + trait_id: 19, + method_id: 1, + }), + }; +} +impl RequestMethod for WorkerEndOperation { + type Request = truapi::versioned::worker::HostWorkerEndOperationRequest; + type Response = truapi::versioned::worker::HostWorkerEndOperationResponse; + type Error = truapi::versioned::worker::HostWorkerEndOperationError; + const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR; +} + +/// Generated `APP_METHODS` execution catalog. +pub const APP_METHODS: &[MethodDescriptor] = &[ + AccountConnectionStatusSubscribe::DESCRIPTOR, + AccountGetAccount::DESCRIPTOR, + AccountGetAccountAlias::DESCRIPTOR, + AccountCreateAccountProof::DESCRIPTOR, + AccountSignVrf::DESCRIPTOR, + AccountRegisterRingVrfKey::DESCRIPTOR, + AccountListRingVrfKeys::DESCRIPTOR, + AccountRingVrfSign::DESCRIPTOR, + AccountGetLegacyAccounts::DESCRIPTOR, + AccountGetUserId::DESCRIPTOR, + AccountRequestLogin::DESCRIPTOR, + ChainFollowHeadSubscribe::DESCRIPTOR, + ChainGetHeadHeader::DESCRIPTOR, + ChainGetHeadBody::DESCRIPTOR, + ChainGetHeadStorage::DESCRIPTOR, + ChainCallHead::DESCRIPTOR, + ChainUnpinHead::DESCRIPTOR, + ChainContinueHead::DESCRIPTOR, + ChainStopHeadOperation::DESCRIPTOR, + ChainGetSpecGenesisHash::DESCRIPTOR, + ChainGetSpecChainName::DESCRIPTOR, + ChainGetSpecProperties::DESCRIPTOR, + ChainBroadcastTransaction::DESCRIPTOR, + ChainStopTransaction::DESCRIPTOR, + ChainGetChainInfo::DESCRIPTOR, + CoinPaymentCreatePurse::DESCRIPTOR, + CoinPaymentQueryPurse::DESCRIPTOR, + CoinPaymentRebalancePurse::DESCRIPTOR, + CoinPaymentDeletePurse::DESCRIPTOR, + CoinPaymentCreateReceivable::DESCRIPTOR, + CoinPaymentCreateCheque::DESCRIPTOR, + CoinPaymentDeposit::DESCRIPTOR, + CoinPaymentRefund::DESCRIPTOR, + CoinPaymentListenForPayment::DESCRIPTOR, + EntropyDerive::DESCRIPTOR, + LocalStorageRead::DESCRIPTOR, + LocalStorageWrite::DESCRIPTOR, + LocalStorageClear::DESCRIPTOR, + LocalStorageSubscribe::DESCRIPTOR, + LocaleSubscribe::DESCRIPTOR, + NotificationsSendPushNotification::DESCRIPTOR, + NotificationsCancelPushNotification::DESCRIPTOR, + PaymentBalanceSubscribe::DESCRIPTOR, + PaymentRequest::DESCRIPTOR, + PaymentStatusSubscribe::DESCRIPTOR, + PaymentTopUp::DESCRIPTOR, + PermissionsRequestDevicePermission::DESCRIPTOR, + PermissionsRequestRemotePermission::DESCRIPTOR, + PermissionsAuthorizeRemotePermission::DESCRIPTOR, + PermissionsAuthorizeDevicePermission::DESCRIPTOR, + PreimageLookupSubscribe::DESCRIPTOR, + PreimageSubmit::DESCRIPTOR, + ResourceAllocationRequest::DESCRIPTOR, + SigningCreateTransaction::DESCRIPTOR, + SigningCreateTransactionWithLegacyAccount::DESCRIPTOR, + SigningSignRawWithLegacyAccount::DESCRIPTOR, + SigningSignPayloadWithLegacyAccount::DESCRIPTOR, + SigningSignRaw::DESCRIPTOR, + SigningSignPayload::DESCRIPTOR, + SigningSignRawUnwatermarkedDeprecated::DESCRIPTOR, + SigningSignRawUnwatermarkedDeprecatedWithLegacyAccount::DESCRIPTOR, + StatementStoreSubscribe::DESCRIPTOR, + StatementStoreCreateProof::DESCRIPTOR, + StatementStoreCreateProofAuthorized::DESCRIPTOR, + StatementStoreSubmit::DESCRIPTOR, + SystemHandshake::DESCRIPTOR, + SystemFeatureSupported::DESCRIPTOR, + SystemNavigateTo::DESCRIPTOR, + SystemHostInfo::DESCRIPTOR, + SystemGetProductContext::DESCRIPTOR, + ThemeSubscribe::DESCRIPTOR, +]; + +/// Generated `WIDGET_METHODS` execution catalog. +pub const WIDGET_METHODS: &[MethodDescriptor] = &[ + AccountConnectionStatusSubscribe::DESCRIPTOR, + AccountGetAccount::DESCRIPTOR, + AccountGetAccountAlias::DESCRIPTOR, + AccountCreateAccountProof::DESCRIPTOR, + AccountSignVrf::DESCRIPTOR, + AccountRegisterRingVrfKey::DESCRIPTOR, + AccountListRingVrfKeys::DESCRIPTOR, + AccountRingVrfSign::DESCRIPTOR, + AccountGetLegacyAccounts::DESCRIPTOR, + AccountGetUserId::DESCRIPTOR, + AccountRequestLogin::DESCRIPTOR, + ChainFollowHeadSubscribe::DESCRIPTOR, + ChainGetHeadHeader::DESCRIPTOR, + ChainGetHeadBody::DESCRIPTOR, + ChainGetHeadStorage::DESCRIPTOR, + ChainCallHead::DESCRIPTOR, + ChainUnpinHead::DESCRIPTOR, + ChainContinueHead::DESCRIPTOR, + ChainStopHeadOperation::DESCRIPTOR, + ChainGetSpecGenesisHash::DESCRIPTOR, + ChainGetSpecChainName::DESCRIPTOR, + ChainGetSpecProperties::DESCRIPTOR, + ChainBroadcastTransaction::DESCRIPTOR, + ChainStopTransaction::DESCRIPTOR, + ChainGetChainInfo::DESCRIPTOR, + CoinPaymentCreatePurse::DESCRIPTOR, + CoinPaymentQueryPurse::DESCRIPTOR, + CoinPaymentRebalancePurse::DESCRIPTOR, + CoinPaymentDeletePurse::DESCRIPTOR, + CoinPaymentCreateReceivable::DESCRIPTOR, + CoinPaymentCreateCheque::DESCRIPTOR, + CoinPaymentDeposit::DESCRIPTOR, + CoinPaymentRefund::DESCRIPTOR, + CoinPaymentListenForPayment::DESCRIPTOR, + EntropyDerive::DESCRIPTOR, + LocalStorageRead::DESCRIPTOR, + LocalStorageWrite::DESCRIPTOR, + LocalStorageClear::DESCRIPTOR, + LocalStorageSubscribe::DESCRIPTOR, + LocaleSubscribe::DESCRIPTOR, + NotificationsSendPushNotification::DESCRIPTOR, + NotificationsCancelPushNotification::DESCRIPTOR, + PaymentBalanceSubscribe::DESCRIPTOR, + PaymentRequest::DESCRIPTOR, + PaymentStatusSubscribe::DESCRIPTOR, + PaymentTopUp::DESCRIPTOR, + PermissionsRequestDevicePermission::DESCRIPTOR, + PermissionsRequestRemotePermission::DESCRIPTOR, + PermissionsAuthorizeRemotePermission::DESCRIPTOR, + PermissionsAuthorizeDevicePermission::DESCRIPTOR, + PreimageLookupSubscribe::DESCRIPTOR, + PreimageSubmit::DESCRIPTOR, + ResourceAllocationRequest::DESCRIPTOR, + SigningCreateTransaction::DESCRIPTOR, + SigningCreateTransactionWithLegacyAccount::DESCRIPTOR, + SigningSignRawWithLegacyAccount::DESCRIPTOR, + SigningSignPayloadWithLegacyAccount::DESCRIPTOR, + SigningSignRaw::DESCRIPTOR, + SigningSignPayload::DESCRIPTOR, + SigningSignRawUnwatermarkedDeprecated::DESCRIPTOR, + SigningSignRawUnwatermarkedDeprecatedWithLegacyAccount::DESCRIPTOR, + StatementStoreSubscribe::DESCRIPTOR, + StatementStoreCreateProof::DESCRIPTOR, + StatementStoreCreateProofAuthorized::DESCRIPTOR, + StatementStoreSubmit::DESCRIPTOR, + SystemHandshake::DESCRIPTOR, + SystemFeatureSupported::DESCRIPTOR, + SystemNavigateTo::DESCRIPTOR, + SystemHostInfo::DESCRIPTOR, + SystemGetProductContext::DESCRIPTOR, + ThemeSubscribe::DESCRIPTOR, +]; + +/// Generated `WORKER_METHODS` execution catalog. +pub const WORKER_METHODS: &[MethodDescriptor] = &[ + AccountConnectionStatusSubscribe::DESCRIPTOR, + AccountGetAccount::DESCRIPTOR, + AccountGetAccountAlias::DESCRIPTOR, + AccountCreateAccountProof::DESCRIPTOR, + AccountSignVrf::DESCRIPTOR, + AccountRegisterRingVrfKey::DESCRIPTOR, + AccountListRingVrfKeys::DESCRIPTOR, + AccountRingVrfSign::DESCRIPTOR, + AccountGetLegacyAccounts::DESCRIPTOR, + AccountGetUserId::DESCRIPTOR, + AccountRequestLogin::DESCRIPTOR, + ChainFollowHeadSubscribe::DESCRIPTOR, + ChainGetHeadHeader::DESCRIPTOR, + ChainGetHeadBody::DESCRIPTOR, + ChainGetHeadStorage::DESCRIPTOR, + ChainCallHead::DESCRIPTOR, + ChainUnpinHead::DESCRIPTOR, + ChainContinueHead::DESCRIPTOR, + ChainStopHeadOperation::DESCRIPTOR, + ChainGetSpecGenesisHash::DESCRIPTOR, + ChainGetSpecChainName::DESCRIPTOR, + ChainGetSpecProperties::DESCRIPTOR, + ChainBroadcastTransaction::DESCRIPTOR, + ChainStopTransaction::DESCRIPTOR, + ChainGetChainInfo::DESCRIPTOR, + ChatCreateRoom::DESCRIPTOR, + ChatRegisterBot::DESCRIPTOR, + ChatListSubscribe::DESCRIPTOR, + ChatPostMessage::DESCRIPTOR, + ChatActionSubscribe::DESCRIPTOR, + CoinPaymentCreatePurse::DESCRIPTOR, + CoinPaymentQueryPurse::DESCRIPTOR, + CoinPaymentRebalancePurse::DESCRIPTOR, + CoinPaymentDeletePurse::DESCRIPTOR, + CoinPaymentCreateReceivable::DESCRIPTOR, + CoinPaymentCreateCheque::DESCRIPTOR, + CoinPaymentDeposit::DESCRIPTOR, + CoinPaymentRefund::DESCRIPTOR, + CoinPaymentListenForPayment::DESCRIPTOR, + EntropyDerive::DESCRIPTOR, + LocalStorageRead::DESCRIPTOR, + LocalStorageWrite::DESCRIPTOR, + LocalStorageClear::DESCRIPTOR, + LocalStorageSubscribe::DESCRIPTOR, + LocaleSubscribe::DESCRIPTOR, + NotificationsSendPushNotification::DESCRIPTOR, + NotificationsCancelPushNotification::DESCRIPTOR, + PaymentBalanceSubscribe::DESCRIPTOR, + PaymentRequest::DESCRIPTOR, + PaymentStatusSubscribe::DESCRIPTOR, + PaymentTopUp::DESCRIPTOR, + PermissionsRequestDevicePermission::DESCRIPTOR, + PermissionsRequestRemotePermission::DESCRIPTOR, + PermissionsAuthorizeRemotePermission::DESCRIPTOR, + PermissionsAuthorizeDevicePermission::DESCRIPTOR, + PocketListSubscribe::DESCRIPTOR, + PocketRemoveCard::DESCRIPTOR, + PreimageLookupSubscribe::DESCRIPTOR, + PreimageSubmit::DESCRIPTOR, + RendererRender::DESCRIPTOR, + RendererActionSubscribe::DESCRIPTOR, + ResourceAllocationRequest::DESCRIPTOR, + SigningCreateTransaction::DESCRIPTOR, + SigningCreateTransactionWithLegacyAccount::DESCRIPTOR, + SigningSignRawWithLegacyAccount::DESCRIPTOR, + SigningSignPayloadWithLegacyAccount::DESCRIPTOR, + SigningSignRaw::DESCRIPTOR, + SigningSignPayload::DESCRIPTOR, + SigningSignRawUnwatermarkedDeprecated::DESCRIPTOR, + SigningSignRawUnwatermarkedDeprecatedWithLegacyAccount::DESCRIPTOR, + StatementStoreSubscribe::DESCRIPTOR, + StatementStoreCreateProof::DESCRIPTOR, + StatementStoreCreateProofAuthorized::DESCRIPTOR, + StatementStoreSubmit::DESCRIPTOR, + SystemHandshake::DESCRIPTOR, + SystemFeatureSupported::DESCRIPTOR, + SystemNavigateTo::DESCRIPTOR, + SystemHostInfo::DESCRIPTOR, + SystemGetProductContext::DESCRIPTOR, + ThemeSubscribe::DESCRIPTOR, + WorkerBeginOperation::DESCRIPTOR, + WorkerEndOperation::DESCRIPTOR, +]; + +/// Generated `WORKER_ONLY_METHODS` execution catalog. +pub const WORKER_ONLY_METHODS: &[MethodDescriptor] = &[ + ChatCreateRoom::DESCRIPTOR, + ChatRegisterBot::DESCRIPTOR, + ChatListSubscribe::DESCRIPTOR, + ChatPostMessage::DESCRIPTOR, + ChatActionSubscribe::DESCRIPTOR, + PocketListSubscribe::DESCRIPTOR, + PocketRemoveCard::DESCRIPTOR, + RendererRender::DESCRIPTOR, + RendererActionSubscribe::DESCRIPTOR, + WorkerBeginOperation::DESCRIPTOR, + WorkerEndOperation::DESCRIPTOR, +]; diff --git a/rust/crates/truapi-client/src/lib.rs b/rust/crates/truapi-client/src/lib.rs new file mode 100644 index 000000000..44080579d --- /dev/null +++ b/rust/crates/truapi-client/src/lib.rs @@ -0,0 +1,626 @@ +#![no_std] + +//! Transport-neutral TrUAPI client codecs and generated method catalog. +//! +//! Frames contain a SCALE request id, a `(trait, method)` address, a message +//! type byte, and the leg's inline SCALE payload. This crate owns no transport. + +extern crate alloc; + +use alloc::string::String; +use alloc::vec::Vec; +use parity_scale_codec::{Decode, Encode}; +use truapi::CallError; + +mod generated; +pub use generated::*; + +/// Product executable kinds used by TrUAPI authorization. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExecutionKind { + /// Full application surface. + App, + /// Embedded widget surface. + Widget, + /// Background product worker. + Worker, +} + +/// Direction in which a method's initial frame travels. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Direction { + /// Product starts the call and the host serves it. + ProductToHost, + /// Host starts the call and the product serves it. + HostToProduct, +} + +/// Wire interaction shape. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MethodKind { + /// One request followed by one response. + Request, + /// Items followed by a normal or failed interruption. + Subscription, +} + +/// The address shared by every leg of one method. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MethodIds { + /// Trait discriminant. + pub trait_id: u8, + /// Method discriminant within the trait. + pub method_id: u8, +} + +/// Which leg of an exchange a frame carries. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum MessageType { + /// Request, or subscription start. + Request = 0, + /// Response, or subscription item. + Response = 1, + /// Subscription completion carrying `Result<(), CallError>`. + Interrupt = 2, + /// Subscription cancellation with no payload. + Stop = 3, +} + +/// Wire ids and interaction shape for one method. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MethodWire { + /// Request/response method. + Request(MethodIds), + /// Subscription method. + Subscription(MethodIds), +} + +/// Generated metadata for one canonical method. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MethodDescriptor { + /// Canonical service trait name. + pub service: &'static str, + /// Rust method name. + pub method: &'static str, + /// Globally unique wire method name. + pub wire_name: &'static str, + /// Rust type of the request envelope. + pub request_type: &'static str, + /// Rust type of the success or stream-item envelope. + pub response_type: &'static str, + /// Rust type of the domain error. + pub error_type: Option<&'static str>, + /// Interaction shape. + pub kind: MethodKind, + /// Initial-frame direction. + pub direction: Direction, + /// Required executable kind, or `None` for every kind. + pub required_execution: Option, + /// Canonical address and shape. + pub wire: MethodWire, +} + +/// Generated marker for a product-initiated request method. +pub trait RequestMethod { + /// Request envelope. + type Request: Encode; + /// Success envelope. + type Response: Decode; + /// Domain-error envelope. + type Error: Decode; + /// Canonical method metadata. + const DESCRIPTOR: MethodDescriptor; +} + +/// Generated marker for a product-initiated subscription. +pub trait SubscriptionMethod { + /// Start request, or unit for an empty payload. + type Request: Encode; + /// Stream item. + type Item: Decode; + /// Domain error carried by the interrupt leg. + type Error: Decode; + /// Canonical method metadata. + const DESCRIPTOR: MethodDescriptor; +} + +/// Generated marker for a host-initiated subscription served by a product. +pub trait HostSubscriptionMethod { + /// Host request. + type Request: Decode; + /// Product stream item. + type Item: Encode; + /// Domain error carried by the interrupt leg. + type Error: Encode; + /// Canonical method metadata. + const DESCRIPTOR: MethodDescriptor; +} + +/// Decoded value paired with its transport request id. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Decoded { + /// Request id from the envelope. + pub request_id: String, + /// Typed payload. + pub value: T, +} + +/// Typed outcome carried by a request response. +pub type RequestOutcome = + Result<::Response, CallError<::Error>>; +/// Result of decoding a request response. +pub type DecodedResponse = Result>, DecodeError>; +/// Typed completion carried by a subscription interruption. +pub type SubscriptionOutcome = Result<(), CallError<::Error>>; + +/// Reserved method-independent protocol error address. +pub const PROTOCOL_ERROR_IDS: MethodIds = MethodIds { + trait_id: 255, + method_id: 255, +}; + +/// A recognized protocol failure from a peer. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProtocolError { + /// The peer cannot route this method address. + UnsupportedMessage(MethodIds), +} + +/// Structural or protocol failure while decoding a frame. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DecodeError { + /// Invalid SCALE or a truncated envelope. + Malformed, + /// Bytes remain after the expected payload. + TrailingBytes, + /// The frame belongs to another method. + UnexpectedMethod { + /// Expected method address. + expected: MethodIds, + /// Actual method address. + actual: MethodIds, + }, + /// The frame belongs to another leg of the exchange. + UnexpectedMessageType { + /// Expected leg. + expected: MessageType, + /// Actual message-type byte. + actual: u8, + }, + /// A descriptor has a shape incompatible with the operation. + WrongMethodKind, + /// A correlated protocol failure. An unknown future version or variant is + /// retained as `None` rather than mistaken for malformed known data. + Protocol { + /// Request id of the call that failed. + request_id: String, + /// Recognized failure, if supported by this build. + error: Option, + }, +} + +/// Encodes a request without an intermediate payload allocation. +pub fn encode_request(request_id: &str, request: &M::Request) -> Vec { + encode_value_frame( + request_id, + generated_request_ids(M::DESCRIPTOR), + MessageType::Request, + request, + ) +} + +/// Decodes a response's `Result>` payload. +pub fn decode_response(frame: &[u8]) -> DecodedResponse { + decode_value_frame( + frame, + generated_request_ids(M::DESCRIPTOR), + MessageType::Response, + ) +} + +/// Encodes a subscription start. +pub fn encode_subscription_start( + request_id: &str, + request: &M::Request, +) -> Vec { + encode_value_frame( + request_id, + generated_subscription_ids(M::DESCRIPTOR), + MessageType::Request, + request, + ) +} + +/// Encodes subscription cancellation; rejects request descriptors. +pub fn encode_subscription_stop( + request_id: &str, + descriptor: MethodDescriptor, +) -> Result, DecodeError> { + Ok(encode_value_frame( + request_id, + subscription_ids(descriptor)?, + MessageType::Stop, + &(), + )) +} + +/// Decodes a subscription item. +pub fn decode_subscription_item( + frame: &[u8], +) -> Result, DecodeError> { + decode_value_frame( + frame, + generated_subscription_ids(M::DESCRIPTOR), + MessageType::Response, + ) +} + +/// Decodes normal completion (`Ok(())`) or a typed subscription failure. +pub fn decode_subscription_interrupt( + frame: &[u8], +) -> Result>, DecodeError> { + decode_value_frame( + frame, + generated_subscription_ids(M::DESCRIPTOR), + MessageType::Interrupt, + ) +} + +/// Whether a frame interrupts this subscription; rejects request descriptors. +pub fn is_subscription_interrupt( + frame: &[u8], + descriptor: MethodDescriptor, +) -> Result { + let ids = subscription_ids(descriptor)?; + let frame = decode_frame(frame)?; + Ok(frame.ids == ids && frame.message_type == MessageType::Interrupt as u8) +} + +/// Decodes a host-initiated start for a product worker. +pub fn decode_host_subscription_start( + frame: &[u8], +) -> Result, DecodeError> { + decode_value_frame( + frame, + generated_subscription_ids(M::DESCRIPTOR), + MessageType::Request, + ) +} + +/// Encodes a product-served subscription item. +pub fn encode_host_subscription_item( + request_id: &str, + item: &M::Item, +) -> Vec { + encode_value_frame( + request_id, + generated_subscription_ids(M::DESCRIPTOR), + MessageType::Response, + item, + ) +} + +/// Encodes normal or failed termination of a product-served subscription. +pub fn encode_host_subscription_interrupt( + request_id: &str, + outcome: &Result<(), CallError>, +) -> Vec { + encode_value_frame( + request_id, + generated_subscription_ids(M::DESCRIPTOR), + MessageType::Interrupt, + outcome, + ) +} + +/// Whether a host frame cancels this product-served subscription. +pub fn is_host_subscription_stop( + frame: &[u8], +) -> Result { + let frame = decode_frame(frame)?; + let matches = frame.ids == generated_subscription_ids(M::DESCRIPTOR) + && frame.message_type == MessageType::Stop as u8; + if matches { + decode_exact::<()>(frame.payload)?; + } + Ok(matches) +} + +fn generated_request_ids(descriptor: MethodDescriptor) -> MethodIds { + match descriptor.wire { + MethodWire::Request(ids) => ids, + MethodWire::Subscription(_) => { + panic!("generated request descriptor must use request wire ids") + } + } +} + +fn subscription_ids(descriptor: MethodDescriptor) -> Result { + match descriptor.wire { + MethodWire::Subscription(ids) => Ok(ids), + MethodWire::Request(_) => Err(DecodeError::WrongMethodKind), + } +} + +fn generated_subscription_ids(descriptor: MethodDescriptor) -> MethodIds { + subscription_ids(descriptor) + .expect("generated subscription descriptor must use subscription wire ids") +} + +fn encode_value_frame( + request_id: &str, + ids: MethodIds, + message_type: MessageType, + value: &T, +) -> Vec { + let mut frame = Vec::new(); + request_id.encode_to(&mut frame); + frame.extend_from_slice(&[ids.trait_id, ids.method_id, message_type as u8]); + value.encode_to(&mut frame); + frame +} + +struct BorrowedFrame<'a> { + request_id: String, + ids: MethodIds, + message_type: u8, + payload: &'a [u8], +} + +fn decode_frame(mut frame: &[u8]) -> Result, DecodeError> { + let request_id = String::decode(&mut frame).map_err(|_| DecodeError::Malformed)?; + let [trait_id, method_id, message_type, payload @ ..] = frame else { + return Err(DecodeError::Malformed); + }; + let ids = MethodIds { + trait_id: *trait_id, + method_id: *method_id, + }; + if ids == PROTOCOL_ERROR_IDS { + if *message_type != MessageType::Response as u8 { + return Err(DecodeError::UnexpectedMessageType { + expected: MessageType::Response, + actual: *message_type, + }); + } + let error = match (payload.first(), payload.get(1)) { + (None, _) => return Err(DecodeError::Malformed), + (Some(version), _) if *version != 0 => None, + (Some(_), Some(variant)) if *variant != 0 => None, + _ => { + let (version, variant, trait_id, method_id): (u8, u8, u8, u8) = + decode_exact(payload)?; + debug_assert_eq!((version, variant), (0, 0)); + Some(ProtocolError::UnsupportedMessage(MethodIds { + trait_id, + method_id, + })) + } + }; + return Err(DecodeError::Protocol { request_id, error }); + } + Ok(BorrowedFrame { + request_id, + ids, + message_type: *message_type, + payload, + }) +} + +fn decode_value_frame( + frame: &[u8], + ids: MethodIds, + message_type: MessageType, +) -> Result, DecodeError> { + let frame = decode_frame(frame)?; + if frame.ids != ids { + return Err(DecodeError::UnexpectedMethod { + expected: ids, + actual: frame.ids, + }); + } + if frame.message_type != message_type as u8 { + return Err(DecodeError::UnexpectedMessageType { + expected: message_type, + actual: frame.message_type, + }); + } + Ok(Decoded { + request_id: frame.request_id, + value: decode_exact(frame.payload)?, + }) +} + +fn decode_exact(mut payload: &[u8]) -> Result { + let value = T::decode(&mut payload).map_err(|_| DecodeError::Malformed)?; + if payload.is_empty() { + Ok(value) + } else { + Err(DecodeError::TrailingBytes) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + use truapi::v01; + use truapi::versioned::system::{HostHandshakeRequest, HostHandshakeResponse}; + + #[test] + fn request_and_response_use_canonical_envelopes() { + let request = HostHandshakeRequest::V1(v01::HostHandshakeRequest { codec_version: 2 }); + assert_eq!( + encode_request::("p:1", &request), + [12, b'p', b':', b'1', 1, 0, 0, 0, 2] + ); + let decoded = decode_response::(&[12, b'p', b':', b'1', 1, 0, 1, 0, 0]) + .expect("response frame"); + assert_eq!(decoded.request_id, "p:1"); + assert_eq!(decoded.value, Ok(HostHandshakeResponse::V1)); + } + + #[test] + fn response_domain_error_preserves_versioned_payload() { + let error = CallError::Domain(truapi::versioned::system::HostHandshakeError::V1( + v01::HostHandshakeError::UnsupportedProtocolVersion, + )); + let mut frame = vec![12, b'p', b':', b'1', 1, 0, 1, 1]; + error.encode_to(&mut frame); + assert_eq!( + decode_response::(&frame) + .expect("error frame") + .value, + Err(error) + ); + } + + #[test] + fn worker_serves_unified_renderer() { + use truapi::versioned::renderer::{ + ProductRendererRenderItem, ProductRendererRenderRequest, + }; + let request = ProductRendererRenderRequest::V1(v01::ProductRendererRenderRequest { + context: v01::RenderContext::PocketCard { + card_id: "loyalty".into(), + }, + payload: vec![1, 2, 3], + }); + let ids = generated_subscription_ids(RendererRender::DESCRIPTOR); + let start = encode_value_frame("host:4", ids, MessageType::Request, &request); + let decoded = + decode_host_subscription_start::(&start).expect("start frame"); + assert_eq!(decoded.request_id, "host:4"); + assert_eq!(decoded.value, request); + let item = ProductRendererRenderItem::V1(v01::RendererNode::Nil); + let rendered = encode_host_subscription_item::("host:4", &item); + assert_eq!( + decode_value_frame::(&rendered, ids, MessageType::Response) + .expect("item frame") + .value, + item + ); + let stop = encode_value_frame("host:4", ids, MessageType::Stop, &()); + assert_eq!(is_host_subscription_stop::(&stop), Ok(true)); + let interrupt = encode_host_subscription_interrupt::("host:4", &Ok(())); + assert_eq!( + decode_value_frame::>>( + &interrupt, + ids, + MessageType::Interrupt + ) + .expect("interrupt") + .value, + Ok(()) + ); + } + + #[test] + fn subscription_completion_and_failure_are_typed_and_strict() { + type Method = AccountConnectionStatusSubscribe; + let ids = generated_subscription_ids(Method::DESCRIPTOR); + let complete = encode_value_frame( + "p:1", + ids, + MessageType::Interrupt, + &Ok::<(), CallError>(()), + ); + assert_eq!( + decode_subscription_interrupt::(&complete) + .expect("completion") + .value, + Ok(()) + ); + let failed = encode_value_frame( + "p:1", + ids, + MessageType::Interrupt, + &Err::<(), _>(CallError::::Denied), + ); + assert_eq!( + decode_subscription_interrupt::(&failed) + .expect("failure") + .value, + Err(CallError::Denied) + ); + let mut trailing = complete; + trailing.push(0); + assert_eq!( + decode_subscription_interrupt::(&trailing), + Err(DecodeError::TrailingBytes) + ); + let empty = encode_value_frame("p:1", ids, MessageType::Interrupt, &()); + assert_eq!( + decode_subscription_interrupt::(&empty), + Err(DecodeError::Malformed) + ); + } + + #[test] + fn frame_address_and_leg_both_must_match() { + let ids = generated_request_ids(SystemHandshake::DESCRIPTOR); + let other = MethodIds { + trait_id: ids.trait_id + 1, + method_id: ids.method_id, + }; + let wrong_method = encode_value_frame( + "p:1", + other, + MessageType::Response, + &Ok::<_, CallError<()>>(HostHandshakeResponse::V1), + ); + assert_eq!( + decode_response::(&wrong_method), + Err(DecodeError::UnexpectedMethod { + expected: ids, + actual: other + }) + ); + let wrong_leg = encode_value_frame("p:1", ids, MessageType::Request, &()); + assert_eq!( + decode_response::(&wrong_leg), + Err(DecodeError::UnexpectedMessageType { + expected: MessageType::Response, + actual: 0 + }) + ); + assert_eq!( + encode_subscription_stop("p:1", SystemHandshake::DESCRIPTOR), + Err(DecodeError::WrongMethodKind) + ); + } + + #[test] + fn protocol_failures_keep_correlation_and_accept_future_variants() { + let known = [12, b'p', b':', b'1', 255, 255, 1, 0, 0, 4, 7]; + assert_eq!( + decode_response::(&known), + Err(DecodeError::Protocol { + request_id: "p:1".into(), + error: Some(ProtocolError::UnsupportedMessage(MethodIds { + trait_id: 4, + method_id: 7 + })) + }) + ); + let future = [12, b'p', b':', b'1', 255, 255, 1, 0, 1, 42]; + assert_eq!( + decode_response::(&future), + Err(DecodeError::Protocol { + request_id: "p:1".into(), + error: None + }) + ); + assert_eq!( + decode_response::(&known[..known.len() - 1]), + Err(DecodeError::Malformed) + ); + let mut trailing = known.to_vec(); + trailing.push(0); + assert_eq!( + decode_response::(&trailing), + Err(DecodeError::TrailingBytes) + ); + } +} diff --git a/rust/crates/truapi-codegen/README.md b/rust/crates/truapi-codegen/README.md index 26552b3cb..3d26fee86 100644 --- a/rust/crates/truapi-codegen/README.md +++ b/rust/crates/truapi-codegen/README.md @@ -1,6 +1,6 @@ # truapi-codegen -_Reads rustdoc JSON for the `truapi` crate and generates the TypeScript client._ +_Reads rustdoc JSON for the `truapi` crate and generates TypeScript clients and Rust codecs._ [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](../../../LICENSE) @@ -9,56 +9,33 @@ _Reads rustdoc JSON for the `truapi` crate and generates the TypeScript client._ - TypeScript types for every protocol type in `truapi`. - TypeScript domain client classes for every unified trait. - The TypeScript wire dispatch table. +- The Rust server dispatcher and transport-neutral wire table. +- The no-std Rust client method catalog when `--rust-client-output` is supplied. ## Generated output -Generated client methods keep API codecs local, encode payload bytes, and hand only wire frames to the transport: - -```ts -export interface TrUApiTransport { - request(params: { - method: string; - payload: Uint8Array; - decodeResponse: (payload: Uint8Array) => Response; - }): Promise; -} - -export class AccountManagementClient { - constructor(private readonly transport: TrUApiTransport) {} - - async accountGet( - request: T.ProductAccountId, - ): Promise> { - const result = await this.transport.request< - S.ResultPayload - >({ - method: "host_account_get", - payload: T.HostAccountGetRequest.enc({ tag: "V2", value: request }), - decodeResponse: (payload) => - S.indexedTaggedUnion({ - V2: [1, S.Result(T.Account, T.RequestCredentialsError)] as const, - }).dec(payload).value, - }); - return result.success ? ok(result.value) : err(result.value); - } -} -``` +Generated methods encode their payloads and pass complete wire frames to the transport. Every frame carries a SCALE request id, a `(trait, method)` byte pair, a `message_type` byte, and the leg's inline payload. Request responses encode `Result>`; subscriptions deliver versioned items and terminate with `Result<(), CallError>`. + +The Rust client catalog targets `truapi-client`: request markers implement `RequestMethod`, product-initiated streams implement `SubscriptionMethod`, and product-served streams such as `Renderer::render` implement `HostSubscriptionMethod`. Both subscription traits declare their domain error through `Error`. All legs share `MethodIds`; the message type distinguishes the leg. ## Architecture The generator runs in three stages: 1. **Parse**: read JSON emitted by nightly rustdoc. -2. **Normalize**: extract the API model, including each method's `#[wire(id = N)]`. -3. **Emit**: generators write TypeScript output. +2. **Normalize**: extract the API model, including `#[wire_trait(id = N)]` and each method's `#[wire(id = N)]`. +3. **Emit**: generators write TypeScript clients and optional Rust dispatcher, wire-table, and client-catalog outputs. -Missing or duplicate wire ids fail generation. One id addresses a method whatever its shape: which leg a frame carries is the envelope's own `message_type` byte. +Missing or duplicate wire ids fail generation. Trait id 255 is reserved for correlated protocol errors. One `(trait, method)` pair addresses a method whatever its shape: which leg a frame carries is the envelope's own `message_type` byte. ## CLI ```bash cargo run -p truapi-codegen -- \ --input target/doc/truapi.json \ + --output js/packages/truapi/src/generated \ + --rust-output rust/crates/truapi-server/src/generated \ + --rust-client-output rust/crates/truapi-client/src/generated.rs --output js/packages/truapi/src/generated ``` @@ -68,11 +45,16 @@ cargo run -p truapi-codegen -- \ cargo +nightly rustdoc -p truapi -- -Z unstable-options --output-format json cargo run -p truapi-codegen -- \ --input target/doc/truapi.json \ + --output js/packages/truapi/src/generated \ + --rust-output rust/crates/truapi-server/src/generated \ + --rust-client-output rust/crates/truapi-client/src/generated.rs --output js/packages/truapi/src/generated ``` The repo wraps both steps in [`scripts/codegen.sh`](../../../scripts/codegen.sh), which is what you should run from the repo root. +CI passes the generated TypeScript and Rust output to the Rust job. `TRUAPI_REQUIRE_GENERATED_TS=1 cargo test -p truapi-server --test wire_table_ts_parity` enforces actual Rust/TypeScript wire-table parity; `cargo test -p truapi-client` exercises client frame codecs after regeneration. Generated Rust source is not compared against checked-in dispatcher or wire-table text snapshots. + ## When to run it Run after any trait or type change in [`truapi`](../truapi/). If you only change runtime behavior without changing the protocol shape, regeneration is not needed. diff --git a/rust/crates/truapi-codegen/src/main.rs b/rust/crates/truapi-codegen/src/main.rs index a4266f0e6..b908cc083 100644 --- a/rust/crates/truapi-codegen/src/main.rs +++ b/rust/crates/truapi-codegen/src/main.rs @@ -63,6 +63,10 @@ struct Cli { #[arg(long)] rust_output: Option, + /// Output file for the generated no-std Rust client catalog (optional). + #[arg(long)] + rust_client_output: Option, + /// Path to rustdoc JSON for the `truapi-platform` crate (optional). /// /// When provided together with `--platform-ts-output`, walks the @@ -169,6 +173,20 @@ fn main() -> Result<()> { .with_context(|| format!("writing Rust dispatcher to {}", path.display()))?; println!("Wrote Rust dispatcher to {}", path.display()); } + if let Some(path) = &cli.rust_client_output { + let schema_hash = + ts::wire_schema_hash(&api, ts::latest_wire_version(&api), cli.codec_version) + .context("computing Rust client wire schema hash")?; + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent) + .with_context(|| format!("creating {}", parent.display()))?; + } + let client = rust::generate_client(&api, &schema_hash) + .context("generating no-std Rust client catalog")?; + std::fs::write(path, client) + .with_context(|| format!("writing Rust client catalog to {}", path.display()))?; + println!("Wrote Rust client catalog to {}", path.display()); + } if let Some(input) = &cli.platform_input { if cli.platform_wasm_adapter_output.is_some() && cli.platform_ts_output.is_none() { anyhow::bail!("--platform-wasm-adapter-output requires --platform-ts-output"); diff --git a/rust/crates/truapi-codegen/src/rust.rs b/rust/crates/truapi-codegen/src/rust.rs index bab69a9eb..8040d6db6 100644 --- a/rust/crates/truapi-codegen/src/rust.rs +++ b/rust/crates/truapi-codegen/src/rust.rs @@ -14,10 +14,12 @@ use convert_case::{Case, Casing}; use crate::platform::PlatformDefinition; use crate::rustdoc::*; +mod client; mod dispatcher; mod wasm_bridge; mod wire_table; +pub use client::generate_client; pub use dispatcher::generate_dispatcher; pub use wasm_bridge::generate_wasm_bridge; pub use wire_table::generate_wire_table; diff --git a/rust/crates/truapi-codegen/src/rust/client.rs b/rust/crates/truapi-codegen/src/rust/client.rs new file mode 100644 index 000000000..931f11bfb --- /dev/null +++ b/rust/crates/truapi-codegen/src/rust/client.rs @@ -0,0 +1,269 @@ +//! no_std Rust client-catalog generation. + +use anyhow::{Result, bail}; +use convert_case::{Case, Casing}; +use std::fmt::Write; + +use super::{module_for_trait, wire_method_name, wire_table}; +use crate::rustdoc::{ApiDefinition, MethodDef, MethodKind, ReturnType, TraitDef, TypeRef}; + +pub fn generate_client(api: &ApiDefinition, schema_hash: &str) -> Result { + let mut out = String::new(); + writeln!(out, "//! Canonical TrUAPI client method catalog.")?; + writeln!(out, "//!")?; + writeln!(out, "//! Auto-generated by truapi-codegen. Do not edit.\n")?; + writeln!(out, "use super::*;\n")?; + writeln!(out, "/// Fingerprint of the generated wire contract.")?; + writeln!( + out, + "pub const TRUAPI_WIRE_SCHEMA_HASH: &str = \"{schema_hash}\";\n" + )?; + + let mut app = Vec::new(); + let mut widget = Vec::new(); + let mut worker = Vec::new(); + let mut worker_only = Vec::new(); + + for trait_name in &api.public_trait_order { + let service = api + .traits + .iter() + .find(|candidate| candidate.name == *trait_name) + .ok_or_else(|| anyhow::anyhow!("public trait `{trait_name}` was not extracted"))?; + let requirement = execution_requirement(service)?; + for method in &service.methods { + let marker = marker_name(service, method); + emit_method(&mut out, service, method, requirement, &marker)?; + match requirement { + None => { + app.push(marker.clone()); + widget.push(marker.clone()); + worker.push(marker); + } + Some("App") => app.push(marker), + Some("Widget") => widget.push(marker), + Some("Worker") => { + worker.push(marker.clone()); + worker_only.push(marker); + } + Some(other) => bail!("unsupported execution requirement `{other}`"), + } + } + } + + emit_catalog(&mut out, "APP_METHODS", &app)?; + emit_catalog(&mut out, "WIDGET_METHODS", &widget)?; + emit_catalog(&mut out, "WORKER_METHODS", &worker)?; + emit_catalog(&mut out, "WORKER_ONLY_METHODS", &worker_only)?; + Ok(out) +} + +fn execution_requirement(service: &TraitDef) -> Result> { + let requirement = service.required_execution(); + if let Some(value) = requirement + && !matches!(value, "App" | "Widget" | "Worker") + { + bail!( + "service `{}` has unknown execution requirement `{value}`", + service.name + ); + } + Ok(requirement) +} + +fn marker_name(service: &TraitDef, method: &MethodDef) -> String { + format!("{}_{}", service.name, method.name).to_case(Case::Pascal) +} + +fn emit_method( + out: &mut String, + service: &TraitDef, + method: &MethodDef, + requirement: Option<&str>, + marker: &str, +) -> Result<()> { + let module = module_for_trait(&service.name); + let wire_name = wire_method_name(&service.name, &method.name); + let execution = requirement + .map(|value| format!("Some(ExecutionKind::{value})")) + .unwrap_or_else(|| "None".to_string()); + let direction = if method.wire.host_initiated { + "Direction::HostToProduct" + } else { + "Direction::ProductToHost" + }; + let kind = match method.kind { + MethodKind::Request => "MethodKind::Request", + MethodKind::Subscription => "MethodKind::Subscription", + }; + let trait_id = wire_table::trait_wire_id(service)?; + let method_id = wire_table::method_wire_id(service, method)?; + let wire_kind = match method.kind { + MethodKind::Request => "Request", + MethodKind::Subscription => "Subscription", + }; + let wire = format!( + "MethodWire::{wire_kind}(MethodIds {{ trait_id: {trait_id}, method_id: {method_id} }})" + ); + let request_name = request_type(method, &module)?; + let (response_name, error_name) = match &method.return_type { + ReturnType::Result { ok, err } => ( + rust_type(ok, &module)?, + Some(rust_type(call_error_domain(err)?, &module)?), + ), + ReturnType::Subscription { item, interrupt } => ( + rust_type(item, &module)?, + Some(rust_type(call_error_domain(interrupt)?, &module)?), + ), + }; + + writeln!(out, "/// `{wire_name}` method marker.")?; + writeln!(out, "pub struct {marker};")?; + writeln!(out, "impl {marker} {{")?; + writeln!( + out, + " /// Canonical metadata and frame ids for this method." + )?; + writeln!( + out, + " pub const DESCRIPTOR: MethodDescriptor = MethodDescriptor {{" + )?; + writeln!(out, " service: \"{}\",", service.name)?; + writeln!(out, " method: \"{}\",", method.name)?; + writeln!(out, " wire_name: \"{wire_name}\",")?; + writeln!(out, " request_type: \"{request_name}\",")?; + writeln!(out, " response_type: \"{response_name}\",")?; + match error_name { + Some(error) => writeln!(out, " error_type: Some(\"{error}\"),")?, + None => writeln!(out, " error_type: None,")?, + } + writeln!(out, " kind: {kind},")?; + writeln!(out, " direction: {direction},")?; + writeln!(out, " required_execution: {execution},")?; + writeln!(out, " wire: {wire},")?; + writeln!(out, " }};")?; + writeln!(out, "}}")?; + + let request = request_type(method, &module)?; + match ( + &method.kind, + &method.return_type, + method.wire.host_initiated, + ) { + (MethodKind::Request, ReturnType::Result { ok, err }, false) => { + let response = rust_type(ok, &module)?; + let error = rust_type(call_error_domain(err)?, &module)?; + writeln!(out, "impl RequestMethod for {marker} {{")?; + writeln!(out, " type Request = {request};")?; + writeln!(out, " type Response = {response};")?; + writeln!(out, " type Error = {error};")?; + writeln!( + out, + " const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR;" + )?; + writeln!(out, "}}\n")?; + } + ( + MethodKind::Subscription, + ReturnType::Subscription { item, interrupt }, + host_initiated, + ) => { + let item = rust_type(item, &module)?; + let error = rust_type(call_error_domain(interrupt)?, &module)?; + let subscription_trait = if host_initiated { + "HostSubscriptionMethod" + } else { + "SubscriptionMethod" + }; + writeln!(out, "impl {subscription_trait} for {marker} {{")?; + writeln!(out, " type Request = {request};")?; + writeln!(out, " type Error = {error};")?; + writeln!(out, " type Item = {item};")?; + writeln!( + out, + " const DESCRIPTOR: MethodDescriptor = Self::DESCRIPTOR;" + )?; + writeln!(out, "}}\n")?; + } + _ => bail!("method `{wire_name}` has unsupported kind/return/direction combination"), + } + Ok(()) +} + +fn request_type(method: &MethodDef, module: &str) -> Result { + match method.params.as_slice() { + [] => Ok("()".to_string()), + [param] => rust_type(¶m.type_ref, module), + params => bail!( + "method `{}` has {} wire parameters; expected at most one request wrapper", + method.name, + params.len() + ), + } +} + +fn call_error_domain(error: &TypeRef) -> Result<&TypeRef> { + match error { + TypeRef::Named { name, args } if name == "CallError" && args.len() == 1 => Ok(&args[0]), + _ => bail!("method error is not CallError: {error:?}"), + } +} + +fn rust_type(value: &TypeRef, module: &str) -> Result { + Ok(match value { + TypeRef::Primitive(name) if name == "String" => "alloc::string::String".to_string(), + TypeRef::Primitive(name) => name.clone(), + TypeRef::Named { name, args } if name == "String" && args.is_empty() => { + "alloc::string::String".to_string() + } + TypeRef::Named { name, args } if name == "Vec" && args.len() == 1 => { + format!("alloc::vec::Vec<{}>", rust_type(&args[0], module)?) + } + TypeRef::Named { name, args } if name == "Option" && args.len() == 1 => { + format!("Option<{}>", rust_type(&args[0], module)?) + } + TypeRef::Named { name, args } if name == "Infallible" && args.is_empty() => { + "core::convert::Infallible".to_string() + } + TypeRef::Named { name, args } if name == "GenericError" && args.is_empty() => { + "truapi::v01::GenericError".to_string() + } + TypeRef::Named { name, args } if args.is_empty() => { + format!("truapi::versioned::{module}::{name}") + } + TypeRef::Named { name, args } => { + let args = args + .iter() + .map(|arg| rust_type(arg, module)) + .collect::>>()? + .join(", "); + format!("truapi::versioned::{module}::{name}<{args}>") + } + TypeRef::Vec(inner) => format!("alloc::vec::Vec<{}>", rust_type(inner, module)?), + TypeRef::Option(inner) => format!("Option<{}>", rust_type(inner, module)?), + TypeRef::Tuple(items) => { + let mut rendered = items + .iter() + .map(|item| rust_type(item, module)) + .collect::>>()? + .join(", "); + if items.len() == 1 { + rendered.push(','); + } + format!("({rendered})") + } + TypeRef::Array(inner, len) => format!("[{}; {len}]", rust_type(inner, module)?), + TypeRef::Generic(name) => bail!("unresolved generic type `{name}` in method surface"), + TypeRef::Unit => "()".to_string(), + }) +} + +fn emit_catalog(out: &mut String, name: &str, markers: &[String]) -> Result<()> { + writeln!(out, "/// Generated `{name}` execution catalog.")?; + writeln!(out, "pub const {name}: &[MethodDescriptor] = &[")?; + for marker in markers { + writeln!(out, " {marker}::DESCRIPTOR,")?; + } + writeln!(out, "];\n")?; + Ok(()) +} diff --git a/rust/crates/truapi-codegen/src/rust/wire_table.rs b/rust/crates/truapi-codegen/src/rust/wire_table.rs index ad46f31a1..51759cb5b 100644 --- a/rust/crates/truapi-codegen/src/rust/wire_table.rs +++ b/rust/crates/truapi-codegen/src/rust/wire_table.rs @@ -106,7 +106,7 @@ pub fn generate_wire_table(api: &ApiDefinition, schema_hash: &str) -> Result Result { +pub(super) fn trait_wire_id(trait_def: &TraitDef) -> Result { trait_def.wire_trait_id.ok_or_else(|| { anyhow::anyhow!( "trait `{}` is missing #[wire_trait(id = N)] annotation", @@ -115,14 +115,18 @@ fn trait_wire_id(trait_def: &TraitDef) -> Result { }) } -fn method_entry(trait_def: &TraitDef, trait_id: u8, method: &MethodDef) -> Result { - let method_id = method.wire.id.ok_or_else(|| { +pub(super) fn method_wire_id(trait_def: &TraitDef, method: &MethodDef) -> Result { + method.wire.id.ok_or_else(|| { anyhow::anyhow!( "method `{}::{}` is missing #[wire(id = N)] annotation", trait_def.name, method.name ) - })?; + }) +} + +fn method_entry(trait_def: &TraitDef, trait_id: u8, method: &MethodDef) -> Result { + let method_id = method_wire_id(trait_def, method)?; let ids = MethodIds { trait_id, method_id, diff --git a/rust/crates/truapi-codegen/tests/golden/dispatcher.rs b/rust/crates/truapi-codegen/tests/golden/dispatcher.rs deleted file mode 100644 index c53f269c2..000000000 --- a/rust/crates/truapi-codegen/tests/golden/dispatcher.rs +++ /dev/null @@ -1,2594 +0,0 @@ -//! Wire dispatcher for the unified `TrUApi` trait. -//! -//! Auto-generated by truapi-codegen. Do not edit. - -// Responses are downgraded to the caller's version uniformly, including -// the methods whose payload is unit and for which the conversion is a -// no-op. -#![allow(clippy::unit_arg)] - -use std::sync::Arc; - -use parity_scale_codec::{DecodeAll, Encode}; - -use truapi::CallContext; -use truapi::api::{ - Account, - Chain, - Chat, - CoinPayment, - Entropy, - LocalStorage, - Locale, - Notifications, - Payment, - Permissions, - Pocket, - Preimage, - Renderer, - ResourceAllocation, - Signing, - StatementStore, - System, - Theme, - Worker, -}; -use truapi::versioned::{self, Versioned}; -use truapi_platform::ProductExecutionKind; - -use crate::dispatcher::Dispatcher; -use crate::frame::downgrade_call_error; -use crate::generated::wire_table; -use crate::subscription::{ - HostInitiatedSubscriptionManager, subscription_interrupt, subscription_stream, -}; -use crate::transport::Transport; - -/// Register every TrUAPI method with the dispatcher. -pub fn register

(dispatcher: &mut Dispatcher, host: Arc

) -where - P: truapi::api::TrUApi + 'static, -{ - register_account(dispatcher, host.clone()); - register_chain(dispatcher, host.clone()); - register_chat(dispatcher, host.clone()); - register_coin_payment(dispatcher, host.clone()); - register_entropy(dispatcher, host.clone()); - register_local_storage(dispatcher, host.clone()); - register_locale(dispatcher, host.clone()); - register_notifications(dispatcher, host.clone()); - register_payment(dispatcher, host.clone()); - register_permissions(dispatcher, host.clone()); - register_pocket(dispatcher, host.clone()); - register_preimage(dispatcher, host.clone()); - register_renderer(dispatcher, host.clone()); - register_resource_allocation(dispatcher, host.clone()); - register_signing(dispatcher, host.clone()); - register_statement_store(dispatcher, host.clone()); - register_system(dispatcher, host.clone()); - register_theme(dispatcher, host.clone()); - register_worker(dispatcher, host); -} - -/// Start the host-initiated `renderer_render` subscription. -pub(crate) fn renderer_render( - subscriptions: &HostInitiatedSubscriptionManager, - transport: Arc, - request: versioned::renderer::ProductRendererRenderRequest, -) -> truapi::Subscription> { - subscriptions.start( - wire_table::RENDERER_RENDER, - parity_scale_codec::Encode::encode(&request), - transport, - ) -} - -fn register_account

(dispatcher: &mut Dispatcher, host: Arc

) -where - P: Account + Send + Sync + 'static, -{ - { - let host = host.clone(); - dispatcher.on_subscription(wire_table::ACCOUNT_CONNECTION_STATUS_SUBSCRIBE, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::account::HostAccountConnectionStatusSubscribeRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Err(subscription_interrupt(error)); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id); - let stream = host.connection_status_subscribe(&cx, request).await; - let stream = futures::StreamExt::map( - stream, - move |item: Result>| { - item.map(|item| { - ::from_latest( - truapi::versioned::IntoLatest::into_latest(item), - target_version, - ) - }) - .map_err(|error| downgrade_call_error(error, target_version)) - }, - ); - Ok(subscription_stream(stream)) - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::ACCOUNT_GET_ACCOUNT, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::account::HostAccountGetRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.get_account(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::ACCOUNT_GET_ACCOUNT_ALIAS, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::account::HostAccountGetAliasRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.get_account_alias(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::ACCOUNT_CREATE_ACCOUNT_PROOF, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::account::HostAccountCreateProofRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.create_account_proof(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::ACCOUNT_SIGN_VRF, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::account::HostAccountSignVrfRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.sign_vrf(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::ACCOUNT_REGISTER_RING_VRF_KEY, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::account::HostAccountRegisterRingVrfKeyRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.register_ring_vrf_key(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::ACCOUNT_LIST_RING_VRF_KEYS, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::account::HostAccountListRingVrfKeysRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.list_ring_vrf_keys(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::ACCOUNT_RING_VRF_SIGN, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::account::HostAccountRingVrfSignRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.ring_vrf_sign(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::ACCOUNT_GET_LEGACY_ACCOUNTS, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::account::HostGetLegacyAccountsRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.get_legacy_accounts(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::ACCOUNT_GET_USER_ID, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::account::HostGetUserIdRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.get_user_id(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host; - dispatcher.on_request(wire_table::ACCOUNT_REQUEST_LOGIN, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::account::HostRequestLoginRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.request_login(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } -} - -fn register_chain

(dispatcher: &mut Dispatcher, host: Arc

) -where - P: Chain + Send + Sync + 'static, -{ - { - let host = host.clone(); - dispatcher.on_subscription(wire_table::CHAIN_FOLLOW_HEAD_SUBSCRIBE, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::chain::RemoteChainHeadFollowRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Err(subscription_interrupt(error)); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id); - let stream = host.follow_head_subscribe(&cx, request).await; - let stream = futures::StreamExt::map( - stream, - move |item: Result>| { - item.map(|item| { - ::from_latest( - truapi::versioned::IntoLatest::into_latest(item), - target_version, - ) - }) - .map_err(|error| downgrade_call_error(error, target_version)) - }, - ); - Ok(subscription_stream(stream)) - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::CHAIN_GET_HEAD_HEADER, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::chain::RemoteChainHeadHeaderRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.get_head_header(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::CHAIN_GET_HEAD_BODY, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::chain::RemoteChainHeadBodyRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.get_head_body(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::CHAIN_GET_HEAD_STORAGE, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::chain::RemoteChainHeadStorageRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.get_head_storage(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::CHAIN_CALL_HEAD, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::chain::RemoteChainHeadCallRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.call_head(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::CHAIN_UNPIN_HEAD, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::chain::RemoteChainHeadUnpinRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.unpin_head(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::CHAIN_CONTINUE_HEAD, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::chain::RemoteChainHeadContinueRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.continue_head(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::CHAIN_STOP_HEAD_OPERATION, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::chain::RemoteChainHeadStopOperationRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.stop_head_operation(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::CHAIN_GET_SPEC_GENESIS_HASH, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::chain::RemoteChainSpecGenesisHashRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.get_spec_genesis_hash(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::CHAIN_GET_SPEC_CHAIN_NAME, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::chain::RemoteChainSpecChainNameRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.get_spec_chain_name(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::CHAIN_GET_SPEC_PROPERTIES, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::chain::RemoteChainSpecPropertiesRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.get_spec_properties(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::CHAIN_BROADCAST_TRANSACTION, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::chain::RemoteChainTransactionBroadcastRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.broadcast_transaction(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::CHAIN_STOP_TRANSACTION, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::chain::RemoteChainTransactionStopRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.stop_transaction(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host; - dispatcher.on_request(wire_table::CHAIN_GET_CHAIN_INFO, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::chain::RemoteChainInfoRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.get_chain_info(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } -} - -fn register_chat

(dispatcher: &mut Dispatcher, host: Arc

) -where - P: Chat + Send + Sync + 'static, -{ - { - let execution_allowed = dispatcher.allows_execution(ProductExecutionKind::Worker); - let host = host.clone(); - dispatcher.on_request(wire_table::CHAT_CREATE_ROOM, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::chat::HostChatCreateRoomRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - if !execution_allowed { - let error: truapi::CallError = truapi::CallError::Denied; - let result: Result> = Err(error); - return result.encode(); - } - let result: Result> = - match host.create_room(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let execution_allowed = dispatcher.allows_execution(ProductExecutionKind::Worker); - let host = host.clone(); - dispatcher.on_request(wire_table::CHAT_REGISTER_BOT, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::chat::HostChatRegisterBotRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - if !execution_allowed { - let error: truapi::CallError = truapi::CallError::Denied; - let result: Result> = Err(error); - return result.encode(); - } - let result: Result> = - match host.register_bot(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let execution_allowed = dispatcher.allows_execution(ProductExecutionKind::Worker); - let host = host.clone(); - dispatcher.on_subscription(wire_table::CHAT_LIST_SUBSCRIBE, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::chat::HostChatListSubscribeRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Err(subscription_interrupt(error)); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id); - if !execution_allowed { - let error: truapi::CallError = truapi::CallError::Denied; - return Err(subscription_interrupt(error)); - } - let stream = host.list_subscribe(&cx, request).await; - let stream = futures::StreamExt::map( - stream, - move |item: Result>| { - item.map(|item| { - ::from_latest( - truapi::versioned::IntoLatest::into_latest(item), - target_version, - ) - }) - .map_err(|error| downgrade_call_error(error, target_version)) - }, - ); - Ok(subscription_stream(stream)) - }) - }); - } - { - let execution_allowed = dispatcher.allows_execution(ProductExecutionKind::Worker); - let host = host.clone(); - dispatcher.on_request(wire_table::CHAT_POST_MESSAGE, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::chat::HostChatPostMessageRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - if !execution_allowed { - let error: truapi::CallError = truapi::CallError::Denied; - let result: Result> = Err(error); - return result.encode(); - } - let result: Result> = - match host.post_message(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let execution_allowed = dispatcher.allows_execution(ProductExecutionKind::Worker); - let host = host; - dispatcher.on_subscription(wire_table::CHAT_ACTION_SUBSCRIBE, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::chat::HostChatActionSubscribeRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Err(subscription_interrupt(error)); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id); - if !execution_allowed { - let error: truapi::CallError = truapi::CallError::Denied; - return Err(subscription_interrupt(error)); - } - let stream = host.action_subscribe(&cx, request).await; - let stream = futures::StreamExt::map( - stream, - move |item: Result>| { - item.map(|item| { - ::from_latest( - truapi::versioned::IntoLatest::into_latest(item), - target_version, - ) - }) - .map_err(|error| downgrade_call_error(error, target_version)) - }, - ); - Ok(subscription_stream(stream)) - }) - }); - } -} - -fn register_coin_payment

(dispatcher: &mut Dispatcher, host: Arc

) -where - P: CoinPayment + Send + Sync + 'static, -{ - { - let host = host.clone(); - dispatcher.on_request(wire_table::COIN_PAYMENT_CREATE_PURSE, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentCreatePurseRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.create_purse(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::COIN_PAYMENT_QUERY_PURSE, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentQueryPurseRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.query_purse(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_subscription(wire_table::COIN_PAYMENT_REBALANCE_PURSE, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentRebalancePurseRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Err(subscription_interrupt(error)); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id); - let stream = host.rebalance_purse(&cx, request).await; - let stream = futures::StreamExt::map( - stream, - move |item: Result>| { - item.map(|item| { - ::from_latest( - truapi::versioned::IntoLatest::into_latest(item), - target_version, - ) - }) - .map_err(|error| downgrade_call_error(error, target_version)) - }, - ); - Ok(subscription_stream(stream)) - }) - }); - } - { - let host = host.clone(); - dispatcher.on_subscription(wire_table::COIN_PAYMENT_DELETE_PURSE, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentDeletePurseRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Err(subscription_interrupt(error)); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id); - let stream = host.delete_purse(&cx, request).await; - let stream = futures::StreamExt::map( - stream, - move |item: Result>| { - item.map(|item| { - ::from_latest( - truapi::versioned::IntoLatest::into_latest(item), - target_version, - ) - }) - .map_err(|error| downgrade_call_error(error, target_version)) - }, - ); - Ok(subscription_stream(stream)) - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::COIN_PAYMENT_CREATE_RECEIVABLE, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentCreateReceivableRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.create_receivable(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::COIN_PAYMENT_CREATE_CHEQUE, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentCreateChequeRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.create_cheque(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_subscription(wire_table::COIN_PAYMENT_DEPOSIT, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentDepositRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Err(subscription_interrupt(error)); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id); - let stream = host.deposit(&cx, request).await; - let stream = futures::StreamExt::map( - stream, - move |item: Result>| { - item.map(|item| { - ::from_latest( - truapi::versioned::IntoLatest::into_latest(item), - target_version, - ) - }) - .map_err(|error| downgrade_call_error(error, target_version)) - }, - ); - Ok(subscription_stream(stream)) - }) - }); - } - { - let host = host.clone(); - dispatcher.on_subscription(wire_table::COIN_PAYMENT_REFUND, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentRefundRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Err(subscription_interrupt(error)); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id); - let stream = host.refund(&cx, request).await; - let stream = futures::StreamExt::map( - stream, - move |item: Result>| { - item.map(|item| { - ::from_latest( - truapi::versioned::IntoLatest::into_latest(item), - target_version, - ) - }) - .map_err(|error| downgrade_call_error(error, target_version)) - }, - ); - Ok(subscription_stream(stream)) - }) - }); - } - { - let host = host; - dispatcher.on_subscription(wire_table::COIN_PAYMENT_LISTEN_FOR_PAYMENT, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::coin_payment::HostCoinPaymentListenForRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Err(subscription_interrupt(error)); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id); - let stream = host.listen_for_payment(&cx, request).await; - let stream = futures::StreamExt::map( - stream, - move |item: Result>| { - item.map(|item| { - ::from_latest( - truapi::versioned::IntoLatest::into_latest(item), - target_version, - ) - }) - .map_err(|error| downgrade_call_error(error, target_version)) - }, - ); - Ok(subscription_stream(stream)) - }) - }); - } -} - -fn register_entropy

(dispatcher: &mut Dispatcher, host: Arc

) -where - P: Entropy + Send + Sync + 'static, -{ - { - let host = host; - dispatcher.on_request(wire_table::ENTROPY_DERIVE, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::entropy::HostDeriveEntropyRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.derive(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } -} - -fn register_local_storage

(dispatcher: &mut Dispatcher, host: Arc

) -where - P: LocalStorage + Send + Sync + 'static, -{ - { - let host = host.clone(); - dispatcher.on_request(wire_table::LOCAL_STORAGE_READ, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::local_storage::HostLocalStorageReadRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.read(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::LOCAL_STORAGE_WRITE, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::local_storage::HostLocalStorageWriteRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.write(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::LOCAL_STORAGE_CLEAR, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::local_storage::HostLocalStorageClearRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.clear(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host; - dispatcher.on_subscription(wire_table::LOCAL_STORAGE_SUBSCRIBE, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::local_storage::HostLocalStorageSubscribeRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Err(subscription_interrupt(error)); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id); - let stream = host.subscribe(&cx, request).await; - let stream = futures::StreamExt::map( - stream, - move |item: Result>| { - item.map(|item| { - ::from_latest( - truapi::versioned::IntoLatest::into_latest(item), - target_version, - ) - }) - .map_err(|error| downgrade_call_error(error, target_version)) - }, - ); - Ok(subscription_stream(stream)) - }) - }); - } -} - -fn register_locale

(dispatcher: &mut Dispatcher, host: Arc

) -where - P: Locale + Send + Sync + 'static, -{ - { - let host = host; - dispatcher.on_subscription(wire_table::LOCALE_SUBSCRIBE, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::locale::HostLocaleSubscribeRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Err(subscription_interrupt(error)); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id); - let stream = host.subscribe(&cx, request).await; - let stream = futures::StreamExt::map( - stream, - move |item: Result>| { - item.map(|item| { - ::from_latest( - truapi::versioned::IntoLatest::into_latest(item), - target_version, - ) - }) - .map_err(|error| downgrade_call_error(error, target_version)) - }, - ); - Ok(subscription_stream(stream)) - }) - }); - } -} - -fn register_notifications

(dispatcher: &mut Dispatcher, host: Arc

) -where - P: Notifications + Send + Sync + 'static, -{ - { - let host = host.clone(); - dispatcher.on_request(wire_table::NOTIFICATIONS_SEND_PUSH_NOTIFICATION, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::notifications::HostPushNotificationRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.send_push_notification(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host; - dispatcher.on_request(wire_table::NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::notifications::HostPushNotificationCancelRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.cancel_push_notification(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } -} - -fn register_payment

(dispatcher: &mut Dispatcher, host: Arc

) -where - P: Payment + Send + Sync + 'static, -{ - { - let host = host.clone(); - dispatcher.on_subscription(wire_table::PAYMENT_BALANCE_SUBSCRIBE, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::payment::HostPaymentBalanceSubscribeRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Err(subscription_interrupt(error)); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id); - let stream = host.balance_subscribe(&cx, request).await; - let stream = futures::StreamExt::map( - stream, - move |item: Result>| { - item.map(|item| { - ::from_latest( - truapi::versioned::IntoLatest::into_latest(item), - target_version, - ) - }) - .map_err(|error| downgrade_call_error(error, target_version)) - }, - ); - Ok(subscription_stream(stream)) - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::PAYMENT_REQUEST, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::payment::HostPaymentRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.request(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_subscription(wire_table::PAYMENT_STATUS_SUBSCRIBE, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::payment::HostPaymentStatusSubscribeRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Err(subscription_interrupt(error)); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id); - let stream = host.status_subscribe(&cx, request).await; - let stream = futures::StreamExt::map( - stream, - move |item: Result>| { - item.map(|item| { - ::from_latest( - truapi::versioned::IntoLatest::into_latest(item), - target_version, - ) - }) - .map_err(|error| downgrade_call_error(error, target_version)) - }, - ); - Ok(subscription_stream(stream)) - }) - }); - } - { - let host = host; - dispatcher.on_request(wire_table::PAYMENT_TOP_UP, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::payment::HostPaymentTopUpRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.top_up(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } -} - -fn register_permissions

(dispatcher: &mut Dispatcher, host: Arc

) -where - P: Permissions + Send + Sync + 'static, -{ - { - let host = host.clone(); - dispatcher.on_request(wire_table::PERMISSIONS_REQUEST_DEVICE_PERMISSION, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::permissions::HostDevicePermissionRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.request_device_permission(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::PERMISSIONS_REQUEST_REMOTE_PERMISSION, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::permissions::RemotePermissionRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.request_remote_permission(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::PERMISSIONS_AUTHORIZE_REMOTE_PERMISSION, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::permissions::RemotePermissionRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.authorize_remote_permission(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host; - dispatcher.on_request(wire_table::PERMISSIONS_AUTHORIZE_DEVICE_PERMISSION, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::permissions::HostDevicePermissionRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.authorize_device_permission(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } -} - -fn register_pocket

(dispatcher: &mut Dispatcher, host: Arc

) -where - P: Pocket + Send + Sync + 'static, -{ - { - let execution_allowed = dispatcher.allows_execution(ProductExecutionKind::Worker); - let host = host.clone(); - dispatcher.on_subscription(wire_table::POCKET_LIST_SUBSCRIBE, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::pocket::HostPocketListSubscribeRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Err(subscription_interrupt(error)); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id); - if !execution_allowed { - let error: truapi::CallError = truapi::CallError::Denied; - return Err(subscription_interrupt(error)); - } - let stream = host.list_subscribe(&cx, request).await; - let stream = futures::StreamExt::map( - stream, - move |item: Result>| { - item.map(|item| { - ::from_latest( - truapi::versioned::IntoLatest::into_latest(item), - target_version, - ) - }) - .map_err(|error| downgrade_call_error(error, target_version)) - }, - ); - Ok(subscription_stream(stream)) - }) - }); - } - { - let execution_allowed = dispatcher.allows_execution(ProductExecutionKind::Worker); - let host = host; - dispatcher.on_request(wire_table::POCKET_REMOVE_CARD, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::pocket::HostPocketRemoveCardRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - if !execution_allowed { - let error: truapi::CallError = truapi::CallError::Denied; - let result: Result> = Err(error); - return result.encode(); - } - let result: Result> = - match host.remove_card(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } -} - -fn register_preimage

(dispatcher: &mut Dispatcher, host: Arc

) -where - P: Preimage + Send + Sync + 'static, -{ - { - let host = host.clone(); - dispatcher.on_subscription(wire_table::PREIMAGE_LOOKUP_SUBSCRIBE, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::preimage::RemotePreimageLookupSubscribeRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Err(subscription_interrupt(error)); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id); - let stream = host.lookup_subscribe(&cx, request).await; - let stream = futures::StreamExt::map( - stream, - move |item: Result>| { - item.map(|item| { - ::from_latest( - truapi::versioned::IntoLatest::into_latest(item), - target_version, - ) - }) - .map_err(|error| downgrade_call_error(error, target_version)) - }, - ); - Ok(subscription_stream(stream)) - }) - }); - } - { - let host = host; - dispatcher.on_request(wire_table::PREIMAGE_SUBMIT, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::preimage::RemotePreimageSubmitRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.submit(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } -} - -fn register_renderer

(dispatcher: &mut Dispatcher, host: Arc

) -where - P: Renderer + Send + Sync + 'static, -{ - { - let execution_allowed = dispatcher.allows_execution(ProductExecutionKind::Worker); - let host = host; - dispatcher.on_subscription(wire_table::RENDERER_ACTION_SUBSCRIBE, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::renderer::HostRendererActionSubscribeRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Err(subscription_interrupt(error)); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id); - if !execution_allowed { - let error: truapi::CallError = truapi::CallError::Denied; - return Err(subscription_interrupt(error)); - } - let stream = host.action_subscribe(&cx, request).await; - let stream = futures::StreamExt::map( - stream, - move |item: Result>| { - item.map(|item| { - ::from_latest( - truapi::versioned::IntoLatest::into_latest(item), - target_version, - ) - }) - .map_err(|error| downgrade_call_error(error, target_version)) - }, - ); - Ok(subscription_stream(stream)) - }) - }); - } -} - -fn register_resource_allocation

(dispatcher: &mut Dispatcher, host: Arc

) -where - P: ResourceAllocation + Send + Sync + 'static, -{ - { - let host = host; - dispatcher.on_request(wire_table::RESOURCE_ALLOCATION_REQUEST, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::resource_allocation::HostRequestResourceAllocationRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.request(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } -} - -fn register_signing

(dispatcher: &mut Dispatcher, host: Arc

) -where - P: Signing + Send + Sync + 'static, -{ - { - let host = host.clone(); - dispatcher.on_request(wire_table::SIGNING_CREATE_TRANSACTION, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::signing::HostCreateTransactionRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.create_transaction(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::signing::HostCreateTransactionWithLegacyAccountRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.create_transaction_with_legacy_account(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::signing::HostSignRawWithLegacyAccountRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.sign_raw_with_legacy_account(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::signing::HostSignPayloadWithLegacyAccountRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.sign_payload_with_legacy_account(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::SIGNING_SIGN_RAW, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::signing::HostSignRawRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.sign_raw(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::SIGNING_SIGN_PAYLOAD, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::signing::HostSignPayloadRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.sign_payload(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::SIGNING_SIGN_RAW_UNWATERMARKED_DEPRECATED, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::signing::HostSignRawRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.sign_raw_unwatermarked_deprecated(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host; - dispatcher.on_request(wire_table::SIGNING_SIGN_RAW_UNWATERMARKED_DEPRECATED_WITH_LEGACY_ACCOUNT, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::signing::HostSignRawWithLegacyAccountRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.sign_raw_unwatermarked_deprecated_with_legacy_account(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } -} - -fn register_statement_store

(dispatcher: &mut Dispatcher, host: Arc

) -where - P: StatementStore + Send + Sync + 'static, -{ - { - let host = host.clone(); - dispatcher.on_subscription(wire_table::STATEMENT_STORE_SUBSCRIBE, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::statement_store::RemoteStatementStoreSubscribeRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Err(subscription_interrupt(error)); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id); - let stream = host.subscribe(&cx, request).await; - let stream = futures::StreamExt::map( - stream, - move |item: Result>| { - item.map(|item| { - ::from_latest( - truapi::versioned::IntoLatest::into_latest(item), - target_version, - ) - }) - .map_err(|error| downgrade_call_error(error, target_version)) - }, - ); - Ok(subscription_stream(stream)) - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::STATEMENT_STORE_CREATE_PROOF, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::statement_store::RemoteStatementStoreCreateProofRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.create_proof(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::STATEMENT_STORE_CREATE_PROOF_AUTHORIZED, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.create_proof_authorized(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host; - dispatcher.on_request(wire_table::STATEMENT_STORE_SUBMIT, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::statement_store::RemoteStatementStoreSubmitRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.submit(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } -} - -fn register_system

(dispatcher: &mut Dispatcher, host: Arc

) -where - P: System + Send + Sync + 'static, -{ - { - let host = host.clone(); - dispatcher.on_request(wire_table::SYSTEM_HANDSHAKE, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::system::HostHandshakeRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.handshake(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::SYSTEM_FEATURE_SUPPORTED, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::system::HostFeatureSupportedRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.feature_supported(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::SYSTEM_NAVIGATE_TO, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::system::HostNavigateToRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.navigate_to(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host.clone(); - dispatcher.on_request(wire_table::SYSTEM_HOST_INFO, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::system::HostInfoRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.host_info(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let host = host; - dispatcher.on_request(wire_table::SYSTEM_GET_PRODUCT_CONTEXT, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::system::HostGetProductContextRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - let result: Result> = - match host.get_product_context(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } -} - -fn register_theme

(dispatcher: &mut Dispatcher, host: Arc

) -where - P: Theme + Send + Sync + 'static, -{ - { - let host = host; - dispatcher.on_subscription(wire_table::THEME_SUBSCRIBE, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::theme::HostThemeSubscribeRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Err(subscription_interrupt(error)); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id); - let stream = host.subscribe(&cx, request).await; - let stream = futures::StreamExt::map( - stream, - move |item: Result>| { - item.map(|item| { - ::from_latest( - truapi::versioned::IntoLatest::into_latest(item), - target_version, - ) - }) - .map_err(|error| downgrade_call_error(error, target_version)) - }, - ); - Ok(subscription_stream(stream)) - }) - }); - } -} - -fn register_worker

(dispatcher: &mut Dispatcher, host: Arc

) -where - P: Worker + Send + Sync + 'static, -{ - { - let execution_allowed = dispatcher.allows_execution(ProductExecutionKind::Worker); - let host = host.clone(); - dispatcher.on_request(wire_table::WORKER_BEGIN_OPERATION, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::worker::HostWorkerBeginOperationRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - if !execution_allowed { - let error: truapi::CallError = truapi::CallError::Denied; - let result: Result> = Err(error); - return result.encode(); - } - let result: Result> = - match host.begin_operation(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } - { - let execution_allowed = dispatcher.allows_execution(ProductExecutionKind::Worker); - let host = host; - dispatcher.on_request(wire_table::WORKER_END_OPERATION, move |request_id: String, bytes: Vec, cancel: truapi::CancellationToken| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::worker::HostWorkerEndOperationRequest = match DecodeAll::decode_all(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - let result: Result> = Err(error); - return result.encode(); - } - }; - let target_version = request.version(); - let cx = CallContext::with_parts(request_id, cancel); - if !execution_allowed { - let error: truapi::CallError = truapi::CallError::Denied; - let result: Result> = Err(error); - return result.encode(); - } - let result: Result> = - match host.end_operation(&cx, request).await { - Ok(response) => Ok(::from_latest( - truapi::versioned::IntoLatest::into_latest(response), - target_version, - )), - Err(err) => Err(downgrade_call_error(err, target_version)), - }; - result.encode() - }) - }); - } -} diff --git a/rust/crates/truapi-codegen/tests/golden/mod.rs b/rust/crates/truapi-codegen/tests/golden/mod.rs deleted file mode 100644 index 770a015d0..000000000 --- a/rust/crates/truapi-codegen/tests/golden/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -//! Generated by truapi-codegen. Do not edit. - -pub mod dispatcher; -pub mod wire_table; diff --git a/rust/crates/truapi-codegen/tests/golden/wire_table.rs b/rust/crates/truapi-codegen/tests/golden/wire_table.rs deleted file mode 100644 index 351e6ebe8..000000000 --- a/rust/crates/truapi-codegen/tests/golden/wire_table.rs +++ /dev/null @@ -1,871 +0,0 @@ -//! Wire-protocol discriminant table. -//! -//! Auto-generated by truapi-codegen. Do not edit. -//! -//! Every frame carries a `(trait, method)` discriminant pair; one -//! method id addresses every frame a method ever sends or receives, -//! regardless of shape. Which leg a frame carries (request/response, -//! or a subscription's start/stop/interrupt/receive) is named by its -//! `message_type` byte, and the payload carries its own version. The -//! ids for each method are exposed as a named const -//! (`PREIMAGE_SUBMIT`, ...); [`WIRE_TABLE`] and the generated -//! dispatcher both reference those consts so the numbers live in -//! exactly one place. The table is sorted by (trait id, method id). - -/// Wire discriminants for one method. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct MethodIds { - /// Trait discriminant carried by every frame of this method. - pub trait_id: u8, - /// Method discriminant carried by every frame of this method. - pub method_id: u8, -} - -/// A single wire-table row. -pub struct WireEntry { - /// Method name from the Rust trait. - pub method: &'static str, - /// What kind of slot this entry describes. - pub kind: WireKind, -} - -/// Wire-slot shape: request/response or a subscription's -/// start/stop/interrupt/receive quartet. -pub enum WireKind { - /// Request/response method. - Request(MethodIds), - /// Subscription method. - Subscription(MethodIds), -} -/// Fingerprint of this build's wire contract: frame ids, method legs, -/// sensitivity, and codec version, identical to the TS client's -/// `TRUAPI_WIRE_SCHEMA_HASH`. A host stamps it on each debug envelope so -/// the debugger refuses to decode a frame whose contract differs from -/// its own, even when the coarse handshake codec version is unchanged. -pub const TRUAPI_WIRE_SCHEMA_HASH: &str = "462dacb6e0d1f504"; - -/// Wire discriminants for `system_handshake`. -pub const SYSTEM_HANDSHAKE: MethodIds = MethodIds { - trait_id: 1, - method_id: 0, -}; - -/// Wire discriminants for `system_feature_supported`. -pub const SYSTEM_FEATURE_SUPPORTED: MethodIds = MethodIds { - trait_id: 1, - method_id: 1, -}; - -/// Wire discriminants for `system_navigate_to`. -pub const SYSTEM_NAVIGATE_TO: MethodIds = MethodIds { - trait_id: 1, - method_id: 2, -}; - -/// Wire discriminants for `system_host_info`. -pub const SYSTEM_HOST_INFO: MethodIds = MethodIds { - trait_id: 1, - method_id: 3, -}; - -/// Wire discriminants for `system_get_product_context`. -pub const SYSTEM_GET_PRODUCT_CONTEXT: MethodIds = MethodIds { - trait_id: 1, - method_id: 4, -}; - -/// Wire discriminants for `account_connection_status_subscribe`. -pub const ACCOUNT_CONNECTION_STATUS_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 2, - method_id: 0, -}; - -/// Wire discriminants for `account_get_account`. -pub const ACCOUNT_GET_ACCOUNT: MethodIds = MethodIds { - trait_id: 2, - method_id: 1, -}; - -/// Wire discriminants for `account_get_account_alias`. -pub const ACCOUNT_GET_ACCOUNT_ALIAS: MethodIds = MethodIds { - trait_id: 2, - method_id: 2, -}; - -/// Wire discriminants for `account_create_account_proof`. -pub const ACCOUNT_CREATE_ACCOUNT_PROOF: MethodIds = MethodIds { - trait_id: 2, - method_id: 3, -}; - -/// Wire discriminants for `account_get_legacy_accounts`. -pub const ACCOUNT_GET_LEGACY_ACCOUNTS: MethodIds = MethodIds { - trait_id: 2, - method_id: 4, -}; - -/// Wire discriminants for `account_get_user_id`. -pub const ACCOUNT_GET_USER_ID: MethodIds = MethodIds { - trait_id: 2, - method_id: 5, -}; - -/// Wire discriminants for `account_request_login`. -pub const ACCOUNT_REQUEST_LOGIN: MethodIds = MethodIds { - trait_id: 2, - method_id: 6, -}; - -/// Wire discriminants for `account_sign_vrf`. -pub const ACCOUNT_SIGN_VRF: MethodIds = MethodIds { - trait_id: 2, - method_id: 7, -}; - -/// Wire discriminants for `account_register_ring_vrf_key`. -pub const ACCOUNT_REGISTER_RING_VRF_KEY: MethodIds = MethodIds { - trait_id: 2, - method_id: 8, -}; - -/// Wire discriminants for `account_list_ring_vrf_keys`. -pub const ACCOUNT_LIST_RING_VRF_KEYS: MethodIds = MethodIds { - trait_id: 2, - method_id: 9, -}; - -/// Wire discriminants for `account_ring_vrf_sign`. -pub const ACCOUNT_RING_VRF_SIGN: MethodIds = MethodIds { - trait_id: 2, - method_id: 10, -}; - -/// Wire discriminants for `chain_follow_head_subscribe`. -pub const CHAIN_FOLLOW_HEAD_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 3, - method_id: 0, -}; - -/// Wire discriminants for `chain_get_head_header`. -pub const CHAIN_GET_HEAD_HEADER: MethodIds = MethodIds { - trait_id: 3, - method_id: 1, -}; - -/// Wire discriminants for `chain_get_head_body`. -pub const CHAIN_GET_HEAD_BODY: MethodIds = MethodIds { - trait_id: 3, - method_id: 2, -}; - -/// Wire discriminants for `chain_get_head_storage`. -pub const CHAIN_GET_HEAD_STORAGE: MethodIds = MethodIds { - trait_id: 3, - method_id: 3, -}; - -/// Wire discriminants for `chain_call_head`. -pub const CHAIN_CALL_HEAD: MethodIds = MethodIds { - trait_id: 3, - method_id: 4, -}; - -/// Wire discriminants for `chain_unpin_head`. -pub const CHAIN_UNPIN_HEAD: MethodIds = MethodIds { - trait_id: 3, - method_id: 5, -}; - -/// Wire discriminants for `chain_continue_head`. -pub const CHAIN_CONTINUE_HEAD: MethodIds = MethodIds { - trait_id: 3, - method_id: 6, -}; - -/// Wire discriminants for `chain_stop_head_operation`. -pub const CHAIN_STOP_HEAD_OPERATION: MethodIds = MethodIds { - trait_id: 3, - method_id: 7, -}; - -/// Wire discriminants for `chain_get_spec_genesis_hash`. -pub const CHAIN_GET_SPEC_GENESIS_HASH: MethodIds = MethodIds { - trait_id: 3, - method_id: 8, -}; - -/// Wire discriminants for `chain_get_spec_chain_name`. -pub const CHAIN_GET_SPEC_CHAIN_NAME: MethodIds = MethodIds { - trait_id: 3, - method_id: 9, -}; - -/// Wire discriminants for `chain_get_spec_properties`. -pub const CHAIN_GET_SPEC_PROPERTIES: MethodIds = MethodIds { - trait_id: 3, - method_id: 10, -}; - -/// Wire discriminants for `chain_broadcast_transaction`. -pub const CHAIN_BROADCAST_TRANSACTION: MethodIds = MethodIds { - trait_id: 3, - method_id: 11, -}; - -/// Wire discriminants for `chain_stop_transaction`. -pub const CHAIN_STOP_TRANSACTION: MethodIds = MethodIds { - trait_id: 3, - method_id: 12, -}; - -/// Wire discriminants for `chain_get_chain_info`. -pub const CHAIN_GET_CHAIN_INFO: MethodIds = MethodIds { - trait_id: 3, - method_id: 13, -}; - -/// Wire discriminants for `chat_create_room`. -pub const CHAT_CREATE_ROOM: MethodIds = MethodIds { - trait_id: 4, - method_id: 0, -}; - -/// Wire discriminants for `chat_register_bot`. -pub const CHAT_REGISTER_BOT: MethodIds = MethodIds { - trait_id: 4, - method_id: 1, -}; - -/// Wire discriminants for `chat_list_subscribe`. -pub const CHAT_LIST_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 4, - method_id: 2, -}; - -/// Wire discriminants for `chat_post_message`. -pub const CHAT_POST_MESSAGE: MethodIds = MethodIds { - trait_id: 4, - method_id: 3, -}; - -/// Wire discriminants for `chat_action_subscribe`. -pub const CHAT_ACTION_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 4, - method_id: 4, -}; - -/// Wire discriminants for `coin_payment_create_purse`. -pub const COIN_PAYMENT_CREATE_PURSE: MethodIds = MethodIds { - trait_id: 5, - method_id: 0, -}; - -/// Wire discriminants for `coin_payment_query_purse`. -pub const COIN_PAYMENT_QUERY_PURSE: MethodIds = MethodIds { - trait_id: 5, - method_id: 1, -}; - -/// Wire discriminants for `coin_payment_rebalance_purse`. -pub const COIN_PAYMENT_REBALANCE_PURSE: MethodIds = MethodIds { - trait_id: 5, - method_id: 2, -}; - -/// Wire discriminants for `coin_payment_delete_purse`. -pub const COIN_PAYMENT_DELETE_PURSE: MethodIds = MethodIds { - trait_id: 5, - method_id: 3, -}; - -/// Wire discriminants for `coin_payment_create_receivable`. -pub const COIN_PAYMENT_CREATE_RECEIVABLE: MethodIds = MethodIds { - trait_id: 5, - method_id: 4, -}; - -/// Wire discriminants for `coin_payment_create_cheque`. -pub const COIN_PAYMENT_CREATE_CHEQUE: MethodIds = MethodIds { - trait_id: 5, - method_id: 5, -}; - -/// Wire discriminants for `coin_payment_deposit`. -pub const COIN_PAYMENT_DEPOSIT: MethodIds = MethodIds { - trait_id: 5, - method_id: 6, -}; - -/// Wire discriminants for `coin_payment_refund`. -pub const COIN_PAYMENT_REFUND: MethodIds = MethodIds { - trait_id: 5, - method_id: 7, -}; - -/// Wire discriminants for `coin_payment_listen_for_payment`. -pub const COIN_PAYMENT_LISTEN_FOR_PAYMENT: MethodIds = MethodIds { - trait_id: 5, - method_id: 8, -}; - -/// Wire discriminants for `entropy_derive`. -pub const ENTROPY_DERIVE: MethodIds = MethodIds { - trait_id: 6, - method_id: 0, -}; - -/// Wire discriminants for `local_storage_read`. -pub const LOCAL_STORAGE_READ: MethodIds = MethodIds { - trait_id: 7, - method_id: 0, -}; - -/// Wire discriminants for `local_storage_write`. -pub const LOCAL_STORAGE_WRITE: MethodIds = MethodIds { - trait_id: 7, - method_id: 1, -}; - -/// Wire discriminants for `local_storage_clear`. -pub const LOCAL_STORAGE_CLEAR: MethodIds = MethodIds { - trait_id: 7, - method_id: 2, -}; - -/// Wire discriminants for `local_storage_subscribe`. -pub const LOCAL_STORAGE_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 7, - method_id: 3, -}; - -/// Wire discriminants for `notifications_send_push_notification`. -pub const NOTIFICATIONS_SEND_PUSH_NOTIFICATION: MethodIds = MethodIds { - trait_id: 8, - method_id: 0, -}; - -/// Wire discriminants for `notifications_cancel_push_notification`. -pub const NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION: MethodIds = MethodIds { - trait_id: 8, - method_id: 1, -}; - -/// Wire discriminants for `payment_balance_subscribe`. -pub const PAYMENT_BALANCE_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 9, - method_id: 0, -}; - -/// Wire discriminants for `payment_top_up`. -pub const PAYMENT_TOP_UP: MethodIds = MethodIds { - trait_id: 9, - method_id: 1, -}; - -/// Wire discriminants for `payment_request`. -pub const PAYMENT_REQUEST: MethodIds = MethodIds { - trait_id: 9, - method_id: 2, -}; - -/// Wire discriminants for `payment_status_subscribe`. -pub const PAYMENT_STATUS_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 9, - method_id: 3, -}; - -/// Wire discriminants for `permissions_request_device_permission`. -pub const PERMISSIONS_REQUEST_DEVICE_PERMISSION: MethodIds = MethodIds { - trait_id: 10, - method_id: 0, -}; - -/// Wire discriminants for `permissions_request_remote_permission`. -pub const PERMISSIONS_REQUEST_REMOTE_PERMISSION: MethodIds = MethodIds { - trait_id: 10, - method_id: 1, -}; - -/// Wire discriminants for `permissions_authorize_remote_permission`. -pub const PERMISSIONS_AUTHORIZE_REMOTE_PERMISSION: MethodIds = MethodIds { - trait_id: 10, - method_id: 2, -}; - -/// Wire discriminants for `permissions_authorize_device_permission`. -pub const PERMISSIONS_AUTHORIZE_DEVICE_PERMISSION: MethodIds = MethodIds { - trait_id: 10, - method_id: 3, -}; - -/// Wire discriminants for `preimage_lookup_subscribe`. -pub const PREIMAGE_LOOKUP_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 11, - method_id: 0, -}; - -/// Wire discriminants for `preimage_submit`. -pub const PREIMAGE_SUBMIT: MethodIds = MethodIds { - trait_id: 11, - method_id: 1, -}; - -/// Wire discriminants for `resource_allocation_request`. -pub const RESOURCE_ALLOCATION_REQUEST: MethodIds = MethodIds { - trait_id: 12, - method_id: 0, -}; - -/// Wire discriminants for `signing_create_transaction`. -pub const SIGNING_CREATE_TRANSACTION: MethodIds = MethodIds { - trait_id: 13, - method_id: 0, -}; - -/// Wire discriminants for `signing_create_transaction_with_legacy_account`. -pub const SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT: MethodIds = MethodIds { - trait_id: 13, - method_id: 1, -}; - -/// Wire discriminants for `signing_sign_raw_with_legacy_account`. -pub const SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT: MethodIds = MethodIds { - trait_id: 13, - method_id: 2, -}; - -/// Wire discriminants for `signing_sign_payload_with_legacy_account`. -pub const SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT: MethodIds = MethodIds { - trait_id: 13, - method_id: 3, -}; - -/// Wire discriminants for `signing_sign_raw`. -pub const SIGNING_SIGN_RAW: MethodIds = MethodIds { - trait_id: 13, - method_id: 4, -}; - -/// Wire discriminants for `signing_sign_payload`. -pub const SIGNING_SIGN_PAYLOAD: MethodIds = MethodIds { - trait_id: 13, - method_id: 5, -}; - -/// Wire discriminants for `signing_sign_raw_unwatermarked_deprecated`. -pub const SIGNING_SIGN_RAW_UNWATERMARKED_DEPRECATED: MethodIds = MethodIds { - trait_id: 13, - method_id: 6, -}; - -/// Wire discriminants for `signing_sign_raw_unwatermarked_deprecated_with_legacy_account`. -pub const SIGNING_SIGN_RAW_UNWATERMARKED_DEPRECATED_WITH_LEGACY_ACCOUNT: MethodIds = MethodIds { - trait_id: 13, - method_id: 7, -}; - -/// Wire discriminants for `statement_store_subscribe`. -pub const STATEMENT_STORE_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 14, - method_id: 0, -}; - -/// Wire discriminants for `statement_store_create_proof`. -pub const STATEMENT_STORE_CREATE_PROOF: MethodIds = MethodIds { - trait_id: 14, - method_id: 1, -}; - -/// Wire discriminants for `statement_store_submit`. -pub const STATEMENT_STORE_SUBMIT: MethodIds = MethodIds { - trait_id: 14, - method_id: 2, -}; - -/// Wire discriminants for `statement_store_create_proof_authorized`. -pub const STATEMENT_STORE_CREATE_PROOF_AUTHORIZED: MethodIds = MethodIds { - trait_id: 14, - method_id: 3, -}; - -/// Wire discriminants for `theme_subscribe`. -pub const THEME_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 15, - method_id: 0, -}; - -/// Wire discriminants for `locale_subscribe`. -pub const LOCALE_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 16, - method_id: 0, -}; - -/// Wire discriminants for `renderer_render`. -pub const RENDERER_RENDER: MethodIds = MethodIds { - trait_id: 17, - method_id: 0, -}; - -/// Wire discriminants for `renderer_action_subscribe`. -pub const RENDERER_ACTION_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 17, - method_id: 1, -}; - -/// Wire discriminants for `pocket_list_subscribe`. -pub const POCKET_LIST_SUBSCRIBE: MethodIds = MethodIds { - trait_id: 18, - method_id: 0, -}; - -/// Wire discriminants for `pocket_remove_card`. -pub const POCKET_REMOVE_CARD: MethodIds = MethodIds { - trait_id: 18, - method_id: 1, -}; - -/// Wire discriminants for `worker_begin_operation`. -pub const WORKER_BEGIN_OPERATION: MethodIds = MethodIds { - trait_id: 19, - method_id: 0, -}; - -/// Wire discriminants for `worker_end_operation`. -pub const WORKER_END_OPERATION: MethodIds = MethodIds { - trait_id: 19, - method_id: 1, -}; - -/// The full wire table. Trait ids and per-trait method ordering are -/// part of the wire protocol; only ever append within a trait. -/// Removed methods leave their slot empty. -pub const WIRE_TABLE: &[WireEntry] = &[ - WireEntry { - method: "system_handshake", - kind: WireKind::Request(SYSTEM_HANDSHAKE), - }, - WireEntry { - method: "system_feature_supported", - kind: WireKind::Request(SYSTEM_FEATURE_SUPPORTED), - }, - WireEntry { - method: "system_navigate_to", - kind: WireKind::Request(SYSTEM_NAVIGATE_TO), - }, - WireEntry { - method: "system_host_info", - kind: WireKind::Request(SYSTEM_HOST_INFO), - }, - WireEntry { - method: "system_get_product_context", - kind: WireKind::Request(SYSTEM_GET_PRODUCT_CONTEXT), - }, - WireEntry { - method: "account_connection_status_subscribe", - kind: WireKind::Subscription(ACCOUNT_CONNECTION_STATUS_SUBSCRIBE), - }, - WireEntry { - method: "account_get_account", - kind: WireKind::Request(ACCOUNT_GET_ACCOUNT), - }, - WireEntry { - method: "account_get_account_alias", - kind: WireKind::Request(ACCOUNT_GET_ACCOUNT_ALIAS), - }, - WireEntry { - method: "account_create_account_proof", - kind: WireKind::Request(ACCOUNT_CREATE_ACCOUNT_PROOF), - }, - WireEntry { - method: "account_get_legacy_accounts", - kind: WireKind::Request(ACCOUNT_GET_LEGACY_ACCOUNTS), - }, - WireEntry { - method: "account_get_user_id", - kind: WireKind::Request(ACCOUNT_GET_USER_ID), - }, - WireEntry { - method: "account_request_login", - kind: WireKind::Request(ACCOUNT_REQUEST_LOGIN), - }, - WireEntry { - method: "account_sign_vrf", - kind: WireKind::Request(ACCOUNT_SIGN_VRF), - }, - WireEntry { - method: "account_register_ring_vrf_key", - kind: WireKind::Request(ACCOUNT_REGISTER_RING_VRF_KEY), - }, - WireEntry { - method: "account_list_ring_vrf_keys", - kind: WireKind::Request(ACCOUNT_LIST_RING_VRF_KEYS), - }, - WireEntry { - method: "account_ring_vrf_sign", - kind: WireKind::Request(ACCOUNT_RING_VRF_SIGN), - }, - WireEntry { - method: "chain_follow_head_subscribe", - kind: WireKind::Subscription(CHAIN_FOLLOW_HEAD_SUBSCRIBE), - }, - WireEntry { - method: "chain_get_head_header", - kind: WireKind::Request(CHAIN_GET_HEAD_HEADER), - }, - WireEntry { - method: "chain_get_head_body", - kind: WireKind::Request(CHAIN_GET_HEAD_BODY), - }, - WireEntry { - method: "chain_get_head_storage", - kind: WireKind::Request(CHAIN_GET_HEAD_STORAGE), - }, - WireEntry { - method: "chain_call_head", - kind: WireKind::Request(CHAIN_CALL_HEAD), - }, - WireEntry { - method: "chain_unpin_head", - kind: WireKind::Request(CHAIN_UNPIN_HEAD), - }, - WireEntry { - method: "chain_continue_head", - kind: WireKind::Request(CHAIN_CONTINUE_HEAD), - }, - WireEntry { - method: "chain_stop_head_operation", - kind: WireKind::Request(CHAIN_STOP_HEAD_OPERATION), - }, - WireEntry { - method: "chain_get_spec_genesis_hash", - kind: WireKind::Request(CHAIN_GET_SPEC_GENESIS_HASH), - }, - WireEntry { - method: "chain_get_spec_chain_name", - kind: WireKind::Request(CHAIN_GET_SPEC_CHAIN_NAME), - }, - WireEntry { - method: "chain_get_spec_properties", - kind: WireKind::Request(CHAIN_GET_SPEC_PROPERTIES), - }, - WireEntry { - method: "chain_broadcast_transaction", - kind: WireKind::Request(CHAIN_BROADCAST_TRANSACTION), - }, - WireEntry { - method: "chain_stop_transaction", - kind: WireKind::Request(CHAIN_STOP_TRANSACTION), - }, - WireEntry { - method: "chain_get_chain_info", - kind: WireKind::Request(CHAIN_GET_CHAIN_INFO), - }, - WireEntry { - method: "chat_create_room", - kind: WireKind::Request(CHAT_CREATE_ROOM), - }, - WireEntry { - method: "chat_register_bot", - kind: WireKind::Request(CHAT_REGISTER_BOT), - }, - WireEntry { - method: "chat_list_subscribe", - kind: WireKind::Subscription(CHAT_LIST_SUBSCRIBE), - }, - WireEntry { - method: "chat_post_message", - kind: WireKind::Request(CHAT_POST_MESSAGE), - }, - WireEntry { - method: "chat_action_subscribe", - kind: WireKind::Subscription(CHAT_ACTION_SUBSCRIBE), - }, - WireEntry { - method: "coin_payment_create_purse", - kind: WireKind::Request(COIN_PAYMENT_CREATE_PURSE), - }, - WireEntry { - method: "coin_payment_query_purse", - kind: WireKind::Request(COIN_PAYMENT_QUERY_PURSE), - }, - WireEntry { - method: "coin_payment_rebalance_purse", - kind: WireKind::Subscription(COIN_PAYMENT_REBALANCE_PURSE), - }, - WireEntry { - method: "coin_payment_delete_purse", - kind: WireKind::Subscription(COIN_PAYMENT_DELETE_PURSE), - }, - WireEntry { - method: "coin_payment_create_receivable", - kind: WireKind::Request(COIN_PAYMENT_CREATE_RECEIVABLE), - }, - WireEntry { - method: "coin_payment_create_cheque", - kind: WireKind::Request(COIN_PAYMENT_CREATE_CHEQUE), - }, - WireEntry { - method: "coin_payment_deposit", - kind: WireKind::Subscription(COIN_PAYMENT_DEPOSIT), - }, - WireEntry { - method: "coin_payment_refund", - kind: WireKind::Subscription(COIN_PAYMENT_REFUND), - }, - WireEntry { - method: "coin_payment_listen_for_payment", - kind: WireKind::Subscription(COIN_PAYMENT_LISTEN_FOR_PAYMENT), - }, - WireEntry { - method: "entropy_derive", - kind: WireKind::Request(ENTROPY_DERIVE), - }, - WireEntry { - method: "local_storage_read", - kind: WireKind::Request(LOCAL_STORAGE_READ), - }, - WireEntry { - method: "local_storage_write", - kind: WireKind::Request(LOCAL_STORAGE_WRITE), - }, - WireEntry { - method: "local_storage_clear", - kind: WireKind::Request(LOCAL_STORAGE_CLEAR), - }, - WireEntry { - method: "local_storage_subscribe", - kind: WireKind::Subscription(LOCAL_STORAGE_SUBSCRIBE), - }, - WireEntry { - method: "notifications_send_push_notification", - kind: WireKind::Request(NOTIFICATIONS_SEND_PUSH_NOTIFICATION), - }, - WireEntry { - method: "notifications_cancel_push_notification", - kind: WireKind::Request(NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION), - }, - WireEntry { - method: "payment_balance_subscribe", - kind: WireKind::Subscription(PAYMENT_BALANCE_SUBSCRIBE), - }, - WireEntry { - method: "payment_top_up", - kind: WireKind::Request(PAYMENT_TOP_UP), - }, - WireEntry { - method: "payment_request", - kind: WireKind::Request(PAYMENT_REQUEST), - }, - WireEntry { - method: "payment_status_subscribe", - kind: WireKind::Subscription(PAYMENT_STATUS_SUBSCRIBE), - }, - WireEntry { - method: "permissions_request_device_permission", - kind: WireKind::Request(PERMISSIONS_REQUEST_DEVICE_PERMISSION), - }, - WireEntry { - method: "permissions_request_remote_permission", - kind: WireKind::Request(PERMISSIONS_REQUEST_REMOTE_PERMISSION), - }, - WireEntry { - method: "permissions_authorize_remote_permission", - kind: WireKind::Request(PERMISSIONS_AUTHORIZE_REMOTE_PERMISSION), - }, - WireEntry { - method: "permissions_authorize_device_permission", - kind: WireKind::Request(PERMISSIONS_AUTHORIZE_DEVICE_PERMISSION), - }, - WireEntry { - method: "preimage_lookup_subscribe", - kind: WireKind::Subscription(PREIMAGE_LOOKUP_SUBSCRIBE), - }, - WireEntry { - method: "preimage_submit", - kind: WireKind::Request(PREIMAGE_SUBMIT), - }, - WireEntry { - method: "resource_allocation_request", - kind: WireKind::Request(RESOURCE_ALLOCATION_REQUEST), - }, - WireEntry { - method: "signing_create_transaction", - kind: WireKind::Request(SIGNING_CREATE_TRANSACTION), - }, - WireEntry { - method: "signing_create_transaction_with_legacy_account", - kind: WireKind::Request(SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT), - }, - WireEntry { - method: "signing_sign_raw_with_legacy_account", - kind: WireKind::Request(SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT), - }, - WireEntry { - method: "signing_sign_payload_with_legacy_account", - kind: WireKind::Request(SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT), - }, - WireEntry { - method: "signing_sign_raw", - kind: WireKind::Request(SIGNING_SIGN_RAW), - }, - WireEntry { - method: "signing_sign_payload", - kind: WireKind::Request(SIGNING_SIGN_PAYLOAD), - }, - WireEntry { - method: "signing_sign_raw_unwatermarked_deprecated", - kind: WireKind::Request(SIGNING_SIGN_RAW_UNWATERMARKED_DEPRECATED), - }, - WireEntry { - method: "signing_sign_raw_unwatermarked_deprecated_with_legacy_account", - kind: WireKind::Request(SIGNING_SIGN_RAW_UNWATERMARKED_DEPRECATED_WITH_LEGACY_ACCOUNT), - }, - WireEntry { - method: "statement_store_subscribe", - kind: WireKind::Subscription(STATEMENT_STORE_SUBSCRIBE), - }, - WireEntry { - method: "statement_store_create_proof", - kind: WireKind::Request(STATEMENT_STORE_CREATE_PROOF), - }, - WireEntry { - method: "statement_store_submit", - kind: WireKind::Request(STATEMENT_STORE_SUBMIT), - }, - WireEntry { - method: "statement_store_create_proof_authorized", - kind: WireKind::Request(STATEMENT_STORE_CREATE_PROOF_AUTHORIZED), - }, - WireEntry { - method: "theme_subscribe", - kind: WireKind::Subscription(THEME_SUBSCRIBE), - }, - WireEntry { - method: "locale_subscribe", - kind: WireKind::Subscription(LOCALE_SUBSCRIBE), - }, - WireEntry { - method: "renderer_render", - kind: WireKind::Subscription(RENDERER_RENDER), - }, - WireEntry { - method: "renderer_action_subscribe", - kind: WireKind::Subscription(RENDERER_ACTION_SUBSCRIBE), - }, - WireEntry { - method: "pocket_list_subscribe", - kind: WireKind::Subscription(POCKET_LIST_SUBSCRIBE), - }, - WireEntry { - method: "pocket_remove_card", - kind: WireKind::Request(POCKET_REMOVE_CARD), - }, - WireEntry { - method: "worker_begin_operation", - kind: WireKind::Request(WORKER_BEGIN_OPERATION), - }, - WireEntry { - method: "worker_end_operation", - kind: WireKind::Request(WORKER_END_OPERATION), - }, -]; diff --git a/rust/crates/truapi-codegen/tests/golden_rust_emit.rs b/rust/crates/truapi-codegen/tests/golden_rust_emit.rs index 2db7ab61e..de9fdae0e 100644 --- a/rust/crates/truapi-codegen/tests/golden_rust_emit.rs +++ b/rust/crates/truapi-codegen/tests/golden_rust_emit.rs @@ -1,4 +1,4 @@ -//! Golden snapshot test for the Rust dispatcher emitter. +//! Integration tests for Rust emission and host-callback generation. //! //! `cargo +nightly rustdoc` runs once per package into a dedicated //! `target/codegen-test-rustdoc/` directory, off the shared @@ -194,73 +194,6 @@ fn prettier_generated(workspace_root: &Path, files: &[PathBuf]) { ); } -#[test] -fn golden_dispatcher_and_wire_table() { - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let workspace = workspace_root(); - - let tempdir = workspace_tempdir(&workspace); - let rustdoc_json = produce_rustdoc_json(&workspace); - - let out = Command::new(env!("CARGO_BIN_EXE_truapi-codegen")) - .args([ - "--input", - rustdoc_json.to_str().unwrap(), - "--output", - tempdir.path().join("ts").to_str().unwrap(), - "--rust-output", - tempdir.path().join("rust").to_str().unwrap(), - ]) - .output() - .expect("run truapi-codegen"); - assert!( - out.status.success(), - "codegen failed: stdout=\n{}\nstderr=\n{}", - String::from_utf8_lossy(&out.stdout), - String::from_utf8_lossy(&out.stderr), - ); - - // Compare the emitted files against the goldens. We assert on - // wire_table.rs first because it's small and the diff is easy to - // read when the wire ids drift. mod.rs is covered because - // truapi-server declares `pub mod generated;` unconditionally, so - // dropping it stops the crate parsing at all. - let golden_dir = manifest_dir.join("tests/golden"); - let cases = [ - ("wire_table.rs", "wire_table.rs"), - ("dispatcher.rs", "dispatcher.rs"), - ("mod.rs", "mod.rs"), - ]; - for (golden_name, output_name) in cases { - let golden = fs::read_to_string(golden_dir.join(golden_name)) - .unwrap_or_else(|e| panic!("read {golden_name}: {e}")); - let actual = fs::read_to_string(tempdir.path().join("rust").join(output_name)) - .unwrap_or_else(|e| panic!("read generated {output_name}: {e}")); - if golden != actual { - // Dump actual to a sibling file for easy inspection - // when running locally. - let dump = manifest_dir.join(format!("tests/golden/{output_name}.actual")); - let _ = fs::write(&dump, &actual); - panic!( - "golden mismatch for {output_name}; wrote actual to {}", - dump.display() - ); - } - } - - // A method body should only ever reference its pre-built `T.{Method}Version` - // codec by name (see `method_envelope_name` in `ts.rs`); `types.ts` - // legitimately inlines `S.indexedTaggedUnion(` to define those wrapper - // codecs themselves, which is why only `client.ts` is scanned here. - let client_ts = fs::read_to_string(tempdir.path().join("ts").join("client.ts")) - .unwrap_or_else(|e| panic!("read generated client.ts: {e}")); - assert!( - !client_ts.contains("indexedTaggedUnion"), - "generated client.ts contains `S.indexedTaggedUnion(`; a method body should \ - reference its `T.{{Method}}Version` codec by name instead of inlining one" - ); -} - /// Idempotence guard at the integration level: running the binary twice /// against the same input must produce identical output. This catches /// non-determinism (HashMap iteration order, timestamps, etc.) that the diff --git a/rust/crates/truapi-host-cli/src/attestation.rs b/rust/crates/truapi-host-cli/src/attestation.rs index 75205b284..72bbe8181 100644 --- a/rust/crates/truapi-host-cli/src/attestation.rs +++ b/rust/crates/truapi-host-cli/src/attestation.rs @@ -17,7 +17,6 @@ use base64::engine::general_purpose::STANDARD as BASE64; use futures_util::{StreamExt as _, TryStreamExt as _, stream}; use serde::Deserialize; use serde_json::{Value, json}; -use sha2::{Digest as _, Sha256}; use std::collections::BTreeSet; use std::sync::Mutex; use tracing::{debug, warn}; @@ -26,8 +25,8 @@ use truapi_server::host_logic::dotns_gateway::{ MAX_BASE_LABEL_LEN, MIN_PERSON_LABEL_LEN, is_registrable_full_label, }; use truapi_server::host_logic::product_account::{ - SR25519_SIGNING_CONTEXT, derive_identity_keypair, derive_root_keypair_from_entropy, - identity_product_id, product_public_key_to_address, + derive_identity_keypair, derive_root_keypair_from_entropy, identity_product_id, + product_public_key_to_address, }; use crate::dotns_read::AssetHubReader; @@ -165,22 +164,14 @@ async fn mint_backend_token( .decode(&challenge) .context("challenge is not valid base64")?; - let keypair = derive_identity_keypair(auth_entropy, network_suffix) - .map_err(|err| anyhow::anyhow!("backend auth identity derivation failed: {err}"))?; - let client_id = keypair.public.to_bytes(); - - // The proof signs SHA256(challenge || clientId || SHA256(body)). It must - // cover the exact bytes the request carries, so the body is serialized once. let payload = b"{}"; - let mut hasher = Sha256::new(); - hasher.update(&challenge_bytes); - hasher.update(client_id); - hasher.update(Sha256::digest(payload)); - let message: [u8; 32] = hasher.finalize().into(); - let proof = keypair - .secret - .sign_simple(SR25519_SIGNING_CONTEXT, &message, &keypair.public) - .to_bytes(); + let (client_id, proof) = truapi_server::host_logic::attestation::sign_backend_challenge( + auth_entropy, + network_suffix, + &challenge_bytes, + payload, + ) + .context("backend auth identity derivation failed")?; let url = format!("{backend_base}/auth/token"); let response = client @@ -586,23 +577,11 @@ async fn submit_registration( ) -> Result<()> { let backend_base = config.backend_base.as_str(); let url = format!("{backend_base}/usernames"); - let mut dotns = json!({ - "signature": hex0x(®.dotns_signature), - "signedAt": signed_at, - }); - if let Some(reserved) = config.reserved_username.as_deref() { - dotns["reservedUsername"] = json!(reserved); - } - let body = json!({ - "username": config.username_base, - "candidateAccountId": reg.candidate_account_id, - "candidateSignature": hex0x(®.candidate_signature), - "ringVrfKey": hex0x(®.ring_vrf_key), - "proofOfOwnership": hex0x(®.proof_of_ownership), - "identifierKey": hex0x(®.identifier_key), - "consumerRegistrationSignature": hex0x(®.consumer_registration_signature), - "dotns": dotns, - }); + let body = reg.request_body( + &config.username_base, + config.reserved_username.as_deref(), + signed_at, + ); let response = send_with_backend_auth( client, backend_base, @@ -628,10 +607,6 @@ async fn submit_registration( bail!("username registration failed ({status}): {text}"); } -fn hex0x(bytes: &[u8]) -> String { - format!("0x{}", hex::encode(bytes)) -} - async fn wait_for_dotns_username( reader: &mut AssetHubReader, candidate: &[u8; 32], diff --git a/rust/crates/truapi-host-cli/src/frame_server.rs b/rust/crates/truapi-host-cli/src/frame_server.rs index 3fa9dae52..c3537319a 100644 --- a/rust/crates/truapi-host-cli/src/frame_server.rs +++ b/rust/crates/truapi-host-cli/src/frame_server.rs @@ -110,6 +110,9 @@ pub trait ProductRuntimeFactory: Send + Sync + 'static { } } +// async-trait's generated boxed Future is already must-use; newer Clippy +// otherwise reports the macro's duplicate annotation on this private trait. +#[allow(clippy::double_must_use)] #[async_trait::async_trait] trait ConnectionRuntime: Send + Sync + 'static { async fn receive_frame(&self, frame: Vec) -> Result<(), ProductRuntimeError>; diff --git a/rust/crates/truapi-macros/tests/ui/sso/fail/request_without_variant.rs b/rust/crates/truapi-macros/tests/ui/sso/fail/request_without_variant.rs deleted file mode 100644 index 6b94b1c0a..000000000 --- a/rust/crates/truapi-macros/tests/ui/sso/fail/request_without_variant.rs +++ /dev/null @@ -1,29 +0,0 @@ -include!("../support/wire.rs"); -include!("../support/runtime.rs"); - -use host_logic::sso::messages::*; -use runtime::sso_service::SsoRequestContext; - -struct Service; -struct MissingRequest; - -#[truapi_macros::sso_service] -impl Service { - async fn foo(&self, _: &SsoRequestContext, _request: Request) -> FooResponse { - Ok(1) - } - - async fn bar(&self, _: &SsoRequestContext, _request: BarRequest) -> BarResponse { - Ok(2) - } - - async fn baz(&self, _: &SsoRequestContext, _request: Box>) -> FooResponse { - Ok(3) - } - - async fn missing(&self, _: &SsoRequestContext, _request: MissingRequest) -> FooResponse { - Ok(4) - } -} - -fn main() {} diff --git a/rust/crates/truapi-macros/tests/ui/sso/fail/request_without_variant.stderr b/rust/crates/truapi-macros/tests/ui/sso/fail/request_without_variant.stderr deleted file mode 100644 index 68b55ace0..000000000 --- a/rust/crates/truapi-macros/tests/ui/sso/fail/request_without_variant.stderr +++ /dev/null @@ -1,10 +0,0 @@ -error[E0599]: no variant, associated function, or constant named `MissingRequest` found for enum `messages::v1::RemoteMessage` in the current scope - --> tests/ui/sso/fail/request_without_variant.rs:24:14 - | -24 | async fn missing(&self, _: &SsoRequestContext, _request: MissingRequest) -> FooResponse { - | ^^^^^^^ variant, associated function, or constant not found in `messages::v1::RemoteMessage` - | - ::: tests/ui/sso/fail/../support/wire.rs - | - | pub enum RemoteMessage { - | ---------------------- variant, associated function, or constant `MissingRequest` not found for this enum diff --git a/rust/crates/truapi-macros/tests/ui/sso/fail/response_without_variant.rs b/rust/crates/truapi-macros/tests/ui/sso/fail/response_without_variant.rs deleted file mode 100644 index 1f30d8345..000000000 --- a/rust/crates/truapi-macros/tests/ui/sso/fail/response_without_variant.rs +++ /dev/null @@ -1,25 +0,0 @@ -include!("../support/wire.rs"); -include!("../support/runtime.rs"); - -use host_logic::sso::messages::*; -use runtime::sso_service::SsoRequestContext; - -type MissingResponse = FooResponse; -struct Service; - -#[truapi_macros::sso_service] -impl Service { - async fn foo(&self, _: &SsoRequestContext, _request: Request) -> MissingResponse { - Ok(1) - } - - async fn bar(&self, _: &SsoRequestContext, _request: BarRequest) -> BarResponse { - Ok(2) - } - - async fn baz(&self, _: &SsoRequestContext, _request: Box>) -> FooResponse { - Ok(3) - } -} - -fn main() {} diff --git a/rust/crates/truapi-macros/tests/ui/sso/fail/response_without_variant.stderr b/rust/crates/truapi-macros/tests/ui/sso/fail/response_without_variant.stderr deleted file mode 100644 index a8914aa6d..000000000 --- a/rust/crates/truapi-macros/tests/ui/sso/fail/response_without_variant.stderr +++ /dev/null @@ -1,10 +0,0 @@ -error[E0599]: no variant, associated function, or constant named `MissingResponse` found for enum `messages::v1::RemoteMessage` in the current scope - --> tests/ui/sso/fail/response_without_variant.rs:12:75 - | -12 | async fn foo(&self, _: &SsoRequestContext, _request: Request) -> MissingResponse { - | ^^^^^^^^^^^^^^^ variant, associated function, or constant not found in `messages::v1::RemoteMessage` - | - ::: tests/ui/sso/fail/../support/wire.rs - | - | pub enum RemoteMessage { - | ---------------------- variant, associated function, or constant `MissingResponse` not found for this enum diff --git a/rust/crates/truapi-polkavm-host/Cargo.toml b/rust/crates/truapi-polkavm-host/Cargo.toml new file mode 100644 index 000000000..dff9f9307 --- /dev/null +++ b/rust/crates/truapi-polkavm-host/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "truapi-polkavm-host" +version = "0.2.0" +edition.workspace = true +description = "Optional composition of the TrUAPI server and PolkaVM host runtime" +license = "MIT" +repository = "https://github.com/paritytech/host-rust-core" +publish = false + +[lib] +crate-type = ["rlib"] + +[dependencies] +polkavm-host-runtime = { git = "https://github.com/paritytech/polkavm-host-runtime.git", rev = "e60a6135be6e00c72c90ca4abe23d2002f4eb962", version = "=0.3.0", features = ["ffi", "native-gpu"] } +truapi-server = { path = "../truapi-server" } + +[lints] +workspace = true diff --git a/rust/crates/truapi-polkavm-host/src/lib.rs b/rust/crates/truapi-polkavm-host/src/lib.rs new file mode 100644 index 000000000..c7dc13bf3 --- /dev/null +++ b/rust/crates/truapi-polkavm-host/src/lib.rs @@ -0,0 +1,16 @@ +//! Optional composition of the TrUAPI server and PolkaVM host runtime. +//! +//! The base [`truapi_server`] remains independent of PolkaVM. Native hosts that +//! need both surfaces link this crate, which pins one reviewed runtime revision. + +/// The pinned PolkaVM host runtime API. +pub use polkavm_host_runtime; +/// The PolkaVM-independent TrUAPI server API. +pub use truapi_server; + +/// Version of this optional composition crate. +pub const TRUAPI_POLKAVM_HOST_VERSION: &str = env!("CARGO_PKG_VERSION"); +/// Version of the pinned PolkaVM host runtime. +pub const POLKAVM_HOST_RUNTIME_VERSION: &str = "0.3.0"; +/// Immutable source revision of the pinned PolkaVM host runtime. +pub const POLKAVM_HOST_RUNTIME_SOURCE_REVISION: &str = "e60a6135be6e00c72c90ca4abe23d2002f4eb962"; diff --git a/rust/crates/truapi-server/src/host_core.rs b/rust/crates/truapi-server/src/host_core.rs index 97713bdd4..3b2c2d16e 100644 --- a/rust/crates/truapi-server/src/host_core.rs +++ b/rust/crates/truapi-server/src/host_core.rs @@ -739,6 +739,73 @@ impl SigningHostRuntime { .map_err(ring_vrf_admin_error) } + /// Read the active local session's X25519 chat identity private key. + #[instrument(skip_all, fields(runtime.method = "signing_host_runtime.session_chat_identity_key"))] + pub fn session_chat_identity_key(&self) -> Option<[u8; 32]> { + self.signing_host + .session_state() + .current()? + .identity_chat_private_key + } + + /// Read this browser's X25519 encryption secret, generating and persisting + /// it on first read. + #[instrument(skip_all, fields(runtime.method = "signing_host_runtime.device_encryption_key"))] + pub async fn device_encryption_key(&self) -> Result<[u8; 32], v01::GenericError> { + self.services + .device_encryption_secret() + .await + .map_err(|reason| v01::GenericError { reason }) + } + + /// Resolve `product_id`'s hard-subtree public key from the active local + /// signing session. + #[instrument(skip_all, fields(runtime.method = "signing_host_runtime.product_subtree_public_key"))] + pub async fn product_subtree_public_key( + &self, + product_id: &str, + timeout_ms: Option, + ) -> Result, v01::GenericError> { + product_subtree_public_key(self.signing_host.as_ref(), product_id, timeout_ms).await + } + + /// Read a stored permission authorization status without prompting. + #[instrument(skip_all, fields(runtime.method = "signing_host_runtime.permission_authorization_status", product_id = %product_id))] + pub async fn permission_authorization_status( + &self, + product_id: &str, + request: PermissionAuthorizationRequest, + ) -> Result { + self.product_admin(product_context(product_id)?) + .permission_authorization_status(request) + .await + } + + /// Read stored permission authorization statuses without prompting. + #[instrument(skip_all, fields(runtime.method = "signing_host_runtime.permission_authorization_statuses", product_id = %product_id))] + pub async fn permission_authorization_statuses( + &self, + product_id: &str, + requests: Vec, + ) -> Result, v01::GenericError> { + self.product_admin(product_context(product_id)?) + .permission_authorization_statuses(requests) + .await + } + + /// Update one stored permission authorization status. + #[instrument(skip_all, fields(runtime.method = "signing_host_runtime.set_permission_authorization_status", product_id = %product_id))] + pub async fn set_permission_authorization_status( + &self, + product_id: &str, + request: PermissionAuthorizationRequest, + status: PermissionAuthorizationStatus, + ) -> Result<(), v01::GenericError> { + self.product_admin(product_context(product_id)?) + .set_permission_authorization_status(request, status) + .await + } + /// Activate a wallet-local session from host-held secret material (raw /// BIP-39 entropy). #[instrument(skip_all, fields(runtime.method = "signing_host_runtime.activate_local_session"))] @@ -767,6 +834,65 @@ impl SigningHostRuntime { }) } + /// Capture the local activation fence for a multi-step identity operation. + pub fn local_identity_context( + &self, + ) -> Result { + self.signing_host.local_identity_context() + } + + /// Sign a backend challenge as the active UID account, with the exact `{}` token body. + pub fn local_identity_auth_proof( + &self, + activation_id: &str, + challenge: &[u8], + ) -> Result, v01::GenericError> { + self.signing_host + .local_identity_auth_proof(activation_id, challenge) + } + + /// Build the backend registration JSON using native proofs and Asset Hub time. + pub async fn local_lite_registration_body( + &self, + activation_id: &str, + username_base: &str, + verifier: [u8; 32], + ) -> Result { + self.signing_host + .local_lite_registration_body(activation_id, username_base, verifier) + .await + } + + /// Refresh authoritative metadata, rejecting a result for a replaced activation. + pub async fn refresh_local_identity_for( + &self, + activation_id: &str, + ) -> Result { + self.signing_host + .refresh_local_identity(activation_id) + .await + } + + /// Resolve and install the active local UID account's on-chain identity. + pub async fn refresh_local_identity( + &self, + ) -> Result { + let context = self.local_identity_context()?; + self.refresh_local_identity_for(&context.activation_id) + .await + } + + /// Inspect current wallet allowances without allocating or mutating identity. + pub async fn get_wallet_allowance_snapshot( + &self, + activation_id: &str, + product_ids: Vec, + ) -> Result { + self.signing_host + .get_wallet_allowance_snapshot(activation_id, product_ids) + .await + } + /// Answer a pairing host's handshake deeplink and serve the resulting SSO /// session until it ends. #[instrument(skip_all, fields(runtime.method = "signing_host_runtime.respond_to_pairing"))] diff --git a/rust/crates/truapi-server/src/host_logic/attestation.rs b/rust/crates/truapi-server/src/host_logic/attestation.rs index 25a8af870..18ac2ddbb 100644 --- a/rust/crates/truapi-server/src/host_logic/attestation.rs +++ b/rust/crates/truapi-server/src/host_logic/attestation.rs @@ -1,4 +1,4 @@ -//! Lite-person username registration parameters (signing host, native only). +//! Lite-person username registration parameters for signing hosts. //! //! Builds the client-side proofs the identity backend needs to //! attest a lite username for an account: an sr25519 proof-of-ownership, a @@ -69,6 +69,57 @@ pub struct LiteRegistration { pub dotns_signature: [u8; 64], } +impl LiteRegistration { + /// Encode the identity backend's registration body, sharing CLI and browser wire bytes. + pub fn request_body( + &self, + username_base: &str, + reserved_username: Option<&str>, + signed_at: u64, + ) -> serde_json::Value { + let hex0x = |bytes: &[u8]| format!("0x{}", hex::encode(bytes)); + let mut dotns = serde_json::json!({ + "signature": hex0x(&self.dotns_signature), + "signedAt": signed_at, + }); + if let Some(reserved) = reserved_username { + dotns["reservedUsername"] = serde_json::json!(reserved); + } + serde_json::json!({ + "username": username_base, + "candidateAccountId": self.candidate_account_id, + "candidateSignature": hex0x(&self.candidate_signature), + "ringVrfKey": hex0x(&self.ring_vrf_key), + "proofOfOwnership": hex0x(&self.proof_of_ownership), + "identifierKey": hex0x(&self.identifier_key), + "consumerRegistrationSignature": hex0x(&self.consumer_registration_signature), + "dotns": dotns, + }) + } +} + +/// Sign the backend auth challenge as the network's UID account, covering the exact body. +pub fn sign_backend_challenge( + entropy: &[u8], + network_suffix: &str, + challenge: &[u8], + body: &[u8], +) -> Result<([u8; 32], [u8; 64]), ProductAccountError> { + use sha2::{Digest as _, Sha256}; + let keypair = derive_identity_keypair(entropy, network_suffix)?; + let client_id = keypair.public.to_bytes(); + let mut hasher = Sha256::new(); + hasher.update(challenge); + hasher.update(client_id); + hasher.update(Sha256::digest(body)); + let message: [u8; 32] = hasher.finalize().into(); + let proof = keypair + .secret + .sign_simple(SR25519_SIGNING_CONTEXT, &message, &keypair.public) + .to_bytes(); + Ok((client_id, proof)) +} + /// Error while building lite-person registration parameters. #[derive(Debug, Error)] pub enum LiteRegistrationError { diff --git a/rust/crates/truapi-server/src/lib.rs b/rust/crates/truapi-server/src/lib.rs index 45f188c17..342da1b4c 100644 --- a/rust/crates/truapi-server/src/lib.rs +++ b/rust/crates/truapi-server/src/lib.rs @@ -71,11 +71,14 @@ pub use native_debug::{DebugSinkError, WsDebugSink}; pub use runtime::StatementRenewalTarget; pub use runtime::login_failure::reports_exhausted_period; pub use runtime::product_manifest::{encode_cached_root_manifest, manifest_cache_key}; +// These helpers use injected host RPC on both native and browser targets. +// Only the direct-URL RPC constructor is native-only. pub use runtime::statement_allowance; pub use runtime::{ AnnouncedPairing, DevicePairingObserver, MAX_PAIRING_METADATA_CHARS, PairedSsoPeer, PairingProposal, PairingProposalMetadata, ResponderExit, }; +pub use runtime::{LocalIdentity, LocalIdentityContext, WalletAllowanceSnapshot}; pub use truapi_platform::{ CoreStorageKeyDescription, CoreStorageKeyDescriptionError, HostRuntimeConfig, PairingHostConfig, PermissionAuthorizationRequest, PermissionAuthorizationStatus, Platform, diff --git a/rust/crates/truapi-server/src/runtime.rs b/rust/crates/truapi-server/src/runtime.rs index 7303ca345..1c8145067 100644 --- a/rust/crates/truapi-server/src/runtime.rs +++ b/rust/crates/truapi-server/src/runtime.rs @@ -65,6 +65,7 @@ pub(crate) use signing_host::{ establish_pairing, notify_pairing_allowance_allocation, notify_pairing_failed, respond_to_pairing, resume_pairing, }; +pub use signing_host::{LocalIdentity, LocalIdentityContext, WalletAllowanceSnapshot}; // `TrackedStatementRenewalTarget` is only read back by the native renewal // reporting, so re-exporting it on wasm leaves an unused import. pub use signing_host::StatementRenewalTarget; diff --git a/rust/crates/truapi-server/src/runtime/bulletin_rpc.rs b/rust/crates/truapi-server/src/runtime/bulletin_rpc.rs index a8c9941ca..76c3530c1 100644 --- a/rust/crates/truapi-server/src/runtime/bulletin_rpc.rs +++ b/rust/crates/truapi-server/src/runtime/bulletin_rpc.rs @@ -244,6 +244,11 @@ impl BulletinRpc { } } + /// The chain used by the allowance reader and submission service. + pub(crate) fn genesis_hash(&self) -> [u8; 32] { + self.genesis_hash + } + /// Open a raw RPC client over the configured Bulletin chain. pub(crate) async fn client( &self, diff --git a/rust/crates/truapi-server/src/runtime/capabilities/account.rs b/rust/crates/truapi-server/src/runtime/capabilities/account.rs index a47617ea1..02434652b 100644 --- a/rust/crates/truapi-server/src/runtime/capabilities/account.rs +++ b/rust/crates/truapi-server/src/runtime/capabilities/account.rs @@ -29,6 +29,7 @@ use truapi_platform::{ use crate::host_logic::product_manifest::Granted; use crate::host_logic::sso::messages::ProductRequest; +use crate::runtime::authority::AuthorityError; use crate::runtime::{ ProductRuntimeHost, account_access_authorization, account_get_authority_error, remote_authority_call, remote_authority_context, ring_vrf_alias_error, ring_vrf_list_error, @@ -81,30 +82,49 @@ impl Account for ProductRuntimeHost { }); } } - } else if self - .authority - .subtree_resolution_reaches_account_holder( - &session, - &product_account_id.dot_ns_identifier, - ) + } else { + // Own-account resolution walks two host callbacks before the + // bounded SSO call: a persisted subtree read, then a confirmation. + // Neither had a deadline, so a host that never answered its own + // storage parked the request forever, with no response and no + // error frame (host-rust-core#954). Both are bounded by the + // caller's context, falling back to the same default the SSO call + // uses, so an unresponsive host surfaces a typed error instead. + let authority_cx = remote_authority_context(cx); + let reaches_account_holder = remote_authority_call(&authority_cx, async { + Ok::<_, AuthorityError>( + self.authority + .subtree_resolution_reaches_account_holder( + &session, + &product_account_id.dot_ns_identifier, + ) + .await, + ) + }) .await - { - // Own-account resolution has no access review, so a cold subtree - // that must reach the Account Holder is the one point a host can - // surface and reject before the SSO call. - let approved = self - .platform - .confirm_user_action(UserConfirmationReview::ProductSubtree( - ProductSubtreeReview { - product_id: product_account_id.dot_ns_identifier.clone(), - }, - )) + .map_err(account_get_authority_error)?; + + if reaches_account_holder { + // Own-account resolution has no access review, so a cold + // subtree that must reach the Account Holder is the one point + // a host can surface and reject before the SSO call. + let approved = remote_authority_call(&authority_cx, async { + self.platform + .confirm_user_action(UserConfirmationReview::ProductSubtree( + ProductSubtreeReview { + product_id: product_account_id.dot_ns_identifier.clone(), + }, + )) + .await + .map_err(|err| AuthorityError::Unavailable { reason: err.reason }) + }) .await - .map_err(|err| CallError::HostFailure { reason: err.reason })?; - if !approved { - return Err(CallError::Domain(HostAccountGetError::V1( - v01::HostAccountGetError::Rejected, - ))); + .map_err(account_get_authority_error)?; + if !approved { + return Err(CallError::Domain(HostAccountGetError::V1( + v01::HostAccountGetError::Rejected, + ))); + } } } diff --git a/rust/crates/truapi-server/src/runtime/identity.rs b/rust/crates/truapi-server/src/runtime/identity.rs index 231c4eaa4..9f5f67511 100644 --- a/rust/crates/truapi-server/src/runtime/identity.rs +++ b/rust/crates/truapi-server/src/runtime/identity.rs @@ -16,7 +16,7 @@ use web_time::Duration; use crate::chain_runtime::ChainRuntime; use crate::host_logic::dotns_gateway::{ - DotnsIdentity, classify_labels, discover_pop_controller, resolve_labels, + DotnsIdentity, DotnsTransport, classify_labels, discover_pop_controller, resolve_labels, }; use crate::host_logic::session::SessionInfo; use crate::runtime::dotns_lookup::DotnsLookup; @@ -174,6 +174,76 @@ async fn lookup_dotns_identity( lookup.await } +/// Resolve the account's Lite identity, requiring gateway ownership as well as PoP provenance. +pub(super) async fn lookup_local_identity( + chain: &ChainRuntime, + genesis: [u8; 32], + account: [u8; 32], +) -> Result { + let operation = async { + let mut lookup = DotnsLookup::pinned_to_best_block( + chain, + genesis, + &format!("local-identity:{}", hex::encode(account)), + ) + .await?; + let controller = discover_pop_controller(&mut lookup) + .await? + .ok_or("dotNS gateway is not deployed on the configured Asset Hub")?; + let labels = resolve_labels(&mut lookup, &controller, &account).await?; + let mut owned = Vec::new(); + for label in labels { + if !crate::host_logic::dotns_gateway::is_dotted_lite_username(&label) { + continue; + } + let owner = lookup + .storage(crate::host_logic::dotns_gateway::lite_label_owner_key( + label.as_bytes(), + )) + .await?; + let Some(owner) = owner else { continue }; + let owner: [u8; 32] = owner + .as_slice() + .try_into() + .map_err(|_| "DotnsGateway.LiteLabelOwner is not a 32-byte account")?; + if owner == account { + owned.push(label); + } + } + classify_labels(&mut lookup, &controller, owned).await + } + .fuse(); + let timeout = futures_timer::Delay::new(LOOKUP_BUDGET).fuse(); + pin_mut!(operation, timeout); + futures::select! { + result = operation => result, + () = timeout => Err("dotNS identity lookup timed out".to_string()), + } +} + +/// Read the reservation timestamp from Asset Hub rather than the browser clock. +pub(super) async fn registration_timestamp( + chain: &ChainRuntime, + genesis: [u8; 32], + account: [u8; 32], +) -> Result { + let mut lookup = DotnsLookup::pinned_to_best_block( + chain, + genesis, + &format!("registration:{}", hex::encode(account)), + ) + .await?; + let value = lookup + .storage(crate::host_logic::dotns_gateway::timestamp_now_key()) + .await? + .ok_or("Timestamp.Now is unset")?; + let millis: [u8; 8] = value + .as_slice() + .try_into() + .map_err(|_| "Timestamp.Now is not a u64")?; + Ok(u64::from_le_bytes(millis) / 1000) +} + #[cfg(all(test, not(target_arch = "wasm32")))] mod tests { //! The in-core lookup drives every dotNS read over one `chainHead_v1` diff --git a/rust/crates/truapi-server/src/runtime/services.rs b/rust/crates/truapi-server/src/runtime/services.rs index daf31a5da..d3b779825 100644 --- a/rust/crates/truapi-server/src/runtime/services.rs +++ b/rust/crates/truapi-server/src/runtime/services.rs @@ -56,8 +56,7 @@ pub(crate) struct RuntimeServices { pub(crate) statement_store: StatementStoreRpc, /// In-core Bulletin submission over the configured Bulletin chain. pub(crate) bulletin: BulletinRpc, - /// Runtime metadata and chain state shared by the native allowance - /// paths, per chain. + /// Runtime metadata and chain state shared by allowance paths, per chain. pub(crate) chain_context: crate::runtime::statement_allowance::ChainContextCache, /// Values from confirmed in-core submissions, served to `lookup_subscribe` /// until the host's content backend has them. Byte-bounded, oldest-first. diff --git a/rust/crates/truapi-server/src/runtime/signing_host.rs b/rust/crates/truapi-server/src/runtime/signing_host.rs index e078ea98d..b4ce3b837 100644 --- a/rust/crates/truapi-server/src/runtime/signing_host.rs +++ b/rust/crates/truapi-server/src/runtime/signing_host.rs @@ -10,17 +10,16 @@ //! signing, v4 transaction construction (payload fields and extensions arrive //! pre-encoded, so no chain metadata is needed), RFC-0007 product entropy, //! bandersnatch ring-VRF aliases and membership proofs, and product-scoped -//! Statement Store and Bulletin allowance keys (native only). - -// Allocation uses `track`; the renewal loop around it is driven by native -// entry points only, so on wasm the rest of the module is not reached yet. -#[cfg_attr(target_arch = "wasm32", allow(dead_code))] +//! Statement Store allowance keys (native and browser), and Bulletin allowance +//! keys (native only). mod allowance_renewal; mod local_activation; +mod local_identity; pub(super) mod ring_vrf; mod sso_replay; mod sso_responder; mod sso_service; +mod wallet_allowances; use std::collections::HashSet; use std::sync::{Arc, Mutex}; @@ -30,10 +29,12 @@ use truapi::latest::{ HostAccountRingVrfSignRequest, ProductAccountId, RingLocation, RingLocationJunction, }; +pub use crate::runtime::statement_allowance::inspection::WalletAllowanceSnapshot; pub use allowance_renewal::StatementRenewalTarget; #[cfg(not(target_arch = "wasm32"))] pub use allowance_renewal::TrackedStatementRenewalTarget; pub(crate) use local_activation::LocalActivation; +pub use local_identity::{LocalIdentity, LocalIdentityContext}; pub use sso_responder::{ AnnouncedPairing, DevicePairingObserver, MAX_PAIRING_METADATA_CHARS, PairedSsoPeer, PairingProposal, PairingProposalMetadata, ResponderExit, @@ -746,6 +747,17 @@ impl ProductAuthority for SigningHost { self.current_local_session() } + async fn refresh_session_identity(&self) -> Option { + let context = self.local_identity_context().ok()?; + if let Err(error) = self.refresh_local_identity(&context.activation_id).await { + tracing::warn!(reason = %error.reason, "local dotNS identity refresh failed"); + } + if self.local_identity_context().ok()?.activation_id != context.activation_id { + return None; + } + self.current_local_session() + } + fn session_state(&self) -> Arc { SigningHost::session_state(self) } diff --git a/rust/crates/truapi-server/src/runtime/signing_host/allowance_renewal.rs b/rust/crates/truapi-server/src/runtime/signing_host/allowance_renewal.rs index c01e0c477..6def5cd58 100644 --- a/rust/crates/truapi-server/src/runtime/signing_host/allowance_renewal.rs +++ b/rust/crates/truapi-server/src/runtime/signing_host/allowance_renewal.rs @@ -6,32 +6,50 @@ //! the active session and runs the chain-pure pass in //! `statement_allowance::renewal`, either once (`renew_now`) or on a periodic //! tick (`start_renewal_loop`). +//! +//! All signing hosts record the ledger during allocation. The resident renewal +//! driver belongs to the native host API; browser hosts currently allocate on +//! demand without starting that driver. +#[cfg(not(target_arch = "wasm32"))] use std::sync::Arc; +#[cfg(not(target_arch = "wasm32"))] use std::sync::atomic::{AtomicBool, Ordering}; +#[cfg(not(target_arch = "wasm32"))] use std::time::Duration; use futures::lock::Mutex; use parity_scale_codec::{Decode, Encode}; -use tracing::{debug, info, warn}; +#[cfg(not(target_arch = "wasm32"))] +use tracing::debug; +#[cfg(any(test, not(target_arch = "wasm32")))] +use tracing::info; +use tracing::warn; use truapi_platform::{CoreStorage, CoreStorageKey}; use super::SigningHost; +#[cfg(not(target_arch = "wasm32"))] use super::sso_responder::current_unix_secs; -use crate::host_logic::product_account::{ - derive_identity_keypair, derive_root_keypair_from_entropy, derive_sr25519_hard_path, -}; +use crate::host_logic::product_account::derive_root_keypair_from_entropy; +use crate::host_logic::product_account::{derive_identity_keypair, derive_sr25519_hard_path}; +#[cfg(not(target_arch = "wasm32"))] use crate::runtime::RuntimeServices; +#[cfg(not(target_arch = "wasm32"))] use crate::runtime::authority::ProductAuthority; +use crate::runtime::statement_allowance::renewal::ResolvedRenewalTarget; +#[cfg(any(test, not(target_arch = "wasm32")))] +use crate::runtime::statement_allowance::renewal::StatementRenewalReport; +#[cfg(not(target_arch = "wasm32"))] use crate::runtime::statement_allowance::renewal::{ - RenewalChainContext, ResolvedRenewalTarget, StatementRenewalReport, next_tick_delay, - renew_targets, + RenewalChainContext, next_tick_delay, renew_targets, }; +#[cfg(not(target_arch = "wasm32"))] use crate::runtime::statement_allowance::{ self, fetch_chain_state, fetch_metadata, find_including_rings, }; /// Fallback tick delay when the system clock is unusable. +#[cfg(not(target_arch = "wasm32"))] const CLOCK_FAILURE_TICK_DELAY: Duration = Duration::from_secs(3_600); /// A statement-store account the signing host promised to keep renewed. @@ -74,6 +92,7 @@ struct LedgerEntry { /// /// Mirrors the persisted entry rather than resolving it: resolution needs root /// entropy, and a host inspecting its slots may hold none. +#[cfg(any(test, not(target_arch = "wasm32")))] #[derive(Clone, Debug, PartialEq, Eq)] pub struct TrackedStatementRenewalTarget { /// The account, or the recipe for one, that the host promised to renew. @@ -95,6 +114,7 @@ impl LedgerEntry { } /// Whether this entry belongs to the identity rooted at `owner`. + #[cfg(any(test, not(target_arch = "wasm32")))] fn is_owned_by(&self, owner: [u8; 32]) -> bool { self.owner.is_none_or(|recorded| recorded == owner) } @@ -117,12 +137,14 @@ pub(super) struct RenewalState { /// Serializes read-modify-write cycles on the ledger so a concurrent /// allocation cannot drop another's entry. ledger_lock: Mutex<()>, + #[cfg(not(target_arch = "wasm32"))] loop_started: AtomicBool, /// The most recent pass, so a host that drives the in-process loop can read /// what it achieved. The loop computes a report on every tick and has no /// caller to hand it to, and exhaustion is the outcome a host most needs to /// act on. A blocking lock rather than an async one: every use is a clone or /// a store with no await in between. + #[cfg(any(test, not(target_arch = "wasm32")))] last_report: std::sync::Mutex>, } @@ -136,6 +158,7 @@ impl RenewalState { } /// Record the pass a host has not seen the return value of. + #[cfg(any(test, not(target_arch = "wasm32")))] fn record_report(&self, report: &StatementRenewalReport) { if let Ok(mut last) = self.last_report.lock() { *last = Some(report.clone()); @@ -144,6 +167,7 @@ impl RenewalState { /// The most recent pass the loop ran. `None` until one has run, which a host /// should read as "not yet" rather than as healthy. + #[cfg(any(test, not(target_arch = "wasm32")))] pub(super) fn last_report(&self) -> Option { self.last_report.lock().ok().and_then(|last| last.clone()) } @@ -200,6 +224,7 @@ async fn track_targets( /// /// Takes the ledger lock so a listing taken while a track or a prune is running /// reports the settled ledger rather than the state it is replacing. +#[cfg(any(test, not(target_arch = "wasm32")))] async fn list_entries( storage: &(impl CoreStorage + ?Sized), ledger_lock: &Mutex<()>, @@ -215,6 +240,7 @@ async fn list_entries( .collect()) } +#[cfg(any(test, not(target_arch = "wasm32")))] async fn untrack_account( storage: &(impl CoreStorage + ?Sized), ledger_lock: &Mutex<()>, @@ -307,6 +333,38 @@ fn resolve_target( } } +/// Resolve attribution without refreshing, pruning, or rewriting the ledger. +pub(super) async fn inspection_labels( + signing_host: &SigningHost, + entropy: &[u8], + owner: [u8; 32], +) -> Result { + let _guard = signing_host.renewal.ledger_lock().lock().await; + let bytes = signing_host + .platform + .read_core_storage(CoreStorageKey::StatementRenewalTargets) + .await + .map_err(|err| format!("renewal ledger read failed: {}", err.reason))?; + let entries = bytes + .as_deref() + .map(decode_entries) + .transpose()? + .unwrap_or_default(); + let mut labels = crate::runtime::statement_allowance::inspection::AccountLabels::new(); + for entry in entries { + if entry.owner.is_some_and(|recorded| recorded != owner) { + continue; + } + let resolved = resolve_target(entropy, &signing_host.network_suffix, &entry.target)?; + let product = match entry.target { + StatementRenewalTarget::ProductStatementAllowance { product_id } => Some(product_id), + _ => None, + }; + labels.insert(resolved.account_id, (product, resolved.label)); + } + Ok(labels) +} + /// Record `targets` in the ledger under the active identity. pub(super) async fn track( signing_host: &SigningHost, @@ -327,6 +385,7 @@ pub(super) async fn track( /// Reads storage alone. A host that has not unlocked an identity still gets /// the list, which is the case a scheduled task runs in: it wakes, asks what /// its finite slots are spent on, and decides whether to renew at all. +#[cfg(any(test, not(target_arch = "wasm32")))] pub(super) async fn list( signing_host: &SigningHost, ) -> Result, String> { @@ -342,12 +401,14 @@ pub(super) async fn list( /// Pairs with [`list`], which reports each entry's owner as stored: comparing /// the two is how a host tells the entries it will actually renew from the ones /// a pass will prune. +#[cfg(any(test, not(target_arch = "wasm32")))] pub(super) fn active_owner_key(signing_host: &SigningHost) -> Result<[u8; 32], String> { let entropy = signing_host.root_entropy().map_err(|err| err.to_string())?; owner_key(&entropy) } /// Stop renewing one fixed statement account for the active identity. +#[cfg(not(target_arch = "wasm32"))] pub(super) async fn untrack_account_for_signing_host( signing_host: &SigningHost, account_id: &[u8; 32], @@ -367,6 +428,7 @@ pub(super) async fn untrack_account_for_signing_host( /// /// A target is skipped rather than failing the pass: one unusable entry must /// not stop every other target from being renewed. +#[cfg(any(test, not(target_arch = "wasm32")))] fn resolve_targets( entropy: &[u8], network_suffix: &str, @@ -401,6 +463,7 @@ fn resolve_targets( /// The labels are logged here rather than only returned. Every step between this /// and the report can fail, and `run_tick` does not read the report at all, so /// the log is the one place a prune is recorded unconditionally. +#[cfg(any(test, not(target_arch = "wasm32")))] async fn owned_targets( storage: &(impl CoreStorage + ?Sized), ledger_lock: &Mutex<()>, @@ -430,6 +493,7 @@ async fn owned_targets( /// One renewal pass: resolve the ledger against the active session and renew /// every target for the current period. +#[cfg(not(target_arch = "wasm32"))] pub(super) async fn renew_now( services: &Arc, signing_host: &SigningHost, @@ -508,6 +572,7 @@ pub(super) async fn renew_now( /// Spawn the periodic renewal loop; repeated calls are no-ops. The loop holds /// only weak references, so it exits when the owning runtime is dropped. +#[cfg(not(target_arch = "wasm32"))] pub(super) fn start_renewal_loop(services: &Arc, signing_host: &Arc) { if signing_host .renewal @@ -538,6 +603,7 @@ pub(super) fn start_renewal_loop(services: &Arc, signing_host: })); } +#[cfg(not(target_arch = "wasm32"))] async fn run_tick(services: &Arc, signing_host: &SigningHost) { if signing_host.root_entropy().is_err() { debug!("skipping statement-store renewal tick; no active session"); @@ -554,6 +620,7 @@ async fn run_tick(services: &Arc, signing_host: &SigningHost) { /// Split out from [`run_tick`] so the recording is reachable without a runtime: a /// loop that logged but forgot to record would leave a host unable to see /// exhaustion, and that is exactly the wiring worth a test. +#[cfg(any(test, not(target_arch = "wasm32")))] fn absorb_tick(state: &RenewalState, result: Result) { match result { Ok(report) => { diff --git a/rust/crates/truapi-server/src/runtime/signing_host/local_identity.rs b/rust/crates/truapi-server/src/runtime/signing_host/local_identity.rs new file mode 100644 index 000000000..2054823a1 --- /dev/null +++ b/rust/crates/truapi-server/src/runtime/signing_host/local_identity.rs @@ -0,0 +1,188 @@ +//! Wallet-local identity proofs and authoritative dotNS metadata refresh. + +use super::SigningHost; +use crate::host_logic::{attestation, dotns_gateway, features}; +use crate::runtime::{connected_session_ui_info, identity}; +use serde::Serialize; +use truapi::latest::{ChainIdentifier, GenericError}; + +/// Verified metadata for the active network's UID account. +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LocalIdentity { + /// Canonical lowercase 0x-prefixed 32-byte account identifier. + pub identity_account_id: String, + /// Lite username confirmed by direct on-chain account lookup. + #[serde(skip_serializing_if = "Option::is_none")] + pub lite_username: Option, +} + +/// Opaque activation fence used throughout an identity backend handshake. +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LocalIdentityContext { + /// Generation token; not an account identifier or secret. + pub activation_id: String, + /// Canonical account identifier that every result must belong to. + pub identity_account_id: String, +} + +fn error(reason: impl ToString) -> GenericError { + GenericError { + reason: reason.to_string(), + } +} + +impl SigningHost { + /// Capture the current UID account and local activation generation atomically. + pub(crate) fn local_identity_context(&self) -> Result { + let state = self + .local_grants + .lock() + .expect("local AutoSigning grant mutex poisoned"); + let account = self + .session_state + .current() + .and_then(|session| session.identity_account_id) + .ok_or_else(|| error("no active local identity"))?; + Ok(LocalIdentityContext { + activation_id: state.activation_generation.to_string(), + identity_account_id: format!("0x{}", hex::encode(account)), + }) + } + + pub(super) fn check_identity_activation( + &self, + activation_id: &str, + ) -> Result<(), GenericError> { + let state = self + .local_grants + .lock() + .expect("local AutoSigning grant mutex poisoned"); + if activation_id.parse::().ok() != Some(state.activation_generation) + || self.session_state.current().is_none() + { + return Err(error("local identity activation changed")); + } + Ok(()) + } + + async fn identity_asset_hub(&self) -> Result<[u8; 32], GenericError> { + let chains = features::supported_chains(self.platform.as_ref()).await?; + features::genesis_for(&chains, ChainIdentifier::AssetHub) + .ok_or_else(|| error("Asset Hub is not configured")) + } + + /// Sign the backend token challenge without releasing UID secret material. + pub(crate) fn local_identity_auth_proof( + &self, + activation_id: &str, + challenge: &[u8], + ) -> Result, GenericError> { + let state = self + .local_grants + .lock() + .expect("local AutoSigning grant mutex poisoned"); + if activation_id.parse::().ok() != Some(state.activation_generation) { + return Err(error("local identity activation changed")); + } + let entropy = self.root_entropy().map_err(error)?; + let (client_id, proof) = + attestation::sign_backend_challenge(&entropy, self.network_suffix(), challenge, b"{}") + .map_err(error)?; + let mut bytes = Vec::with_capacity(96); + bytes.extend_from_slice(&client_id); + bytes.extend_from_slice(&proof); + Ok(bytes) + } + + /// Build shared registration JSON with proofs timestamped at the configured Asset Hub. + pub(crate) async fn local_lite_registration_body( + &self, + activation_id: &str, + username_base: &str, + verifier: [u8; 32], + ) -> Result { + if !dotns_gateway::is_registrable_full_label(username_base) + || username_base.len() + 3 > dotns_gateway::MAX_BASE_LABEL_LEN + { + return Err(error( + "Lite username base must contain 6 to 29 lowercase ASCII letters", + )); + } + self.check_identity_activation(activation_id)?; + let genesis = self.identity_asset_hub().await?; + self.check_identity_activation(activation_id)?; + let account = self + .session_state + .current() + .and_then(|session| session.identity_account_id) + .ok_or_else(|| error("no active local identity"))?; + let signed_at = identity::registration_timestamp(&self.services.chain, genesis, account) + .await + .map_err(error)?; + let state = self + .local_grants + .lock() + .expect("local AutoSigning grant mutex poisoned"); + if activation_id.parse::().ok() != Some(state.activation_generation) { + return Err(error("local identity activation changed")); + } + let entropy = self.root_entropy().map_err(error)?; + let registration = attestation::build_lite_registration( + &entropy, + self.network_suffix(), + verifier, + username_base, + None, + signed_at, + ) + .map_err(error)?; + Ok(registration + .request_body(username_base, None, signed_at) + .to_string()) + } + + /// Install chain-verified Lite metadata only if the local activation remains current. + pub(crate) async fn refresh_local_identity( + &self, + activation_id: &str, + ) -> Result { + self.check_identity_activation(activation_id)?; + let genesis = self.identity_asset_hub().await?; + self.check_identity_activation(activation_id)?; + let account = self + .session_state + .current() + .and_then(|session| session.identity_account_id) + .ok_or_else(|| error("no active local identity"))?; + let resolved = identity::lookup_local_identity(&self.services.chain, genesis, account) + .await + .map_err(error)?; + let state = self + .local_grants + .lock() + .expect("local AutoSigning grant mutex poisoned"); + if activation_id.parse::().ok() != Some(state.activation_generation) { + return Err(error("local identity activation changed")); + } + let mut session = self + .session_state + .current() + .ok_or_else(|| error("no active local identity"))?; + if session.identity_account_id != Some(account) { + return Err(error("local identity account changed")); + } + session.lite_username = resolved.lite_username; + session.full_username = resolved.full_username; + let result = LocalIdentity { + identity_account_id: format!("0x{}", hex::encode(account)), + lite_username: session.lite_username.clone(), + }; + let ui_info = connected_session_ui_info(&session); + self.session_state.set_session(session); + drop(state); + self.auth_state.connected(&ui_info); + Ok(result) + } +} diff --git a/rust/crates/truapi-server/src/runtime/signing_host/wallet_allowances.rs b/rust/crates/truapi-server/src/runtime/signing_host/wallet_allowances.rs new file mode 100644 index 000000000..40247801e --- /dev/null +++ b/rust/crates/truapi-server/src/runtime/signing_host/wallet_allowances.rs @@ -0,0 +1,273 @@ +//! Trusted wallet-admin inspection; intentionally outside the product protocol. + +use std::collections::HashSet; + +use truapi::latest::{ChainIdentifier, GenericError}; +use truapi_platform::normalize_product_identifier; + +use super::{SigningHost, allowance_renewal}; +use crate::host_logic::{ + features, + product_account::{ + derive_product_keypair, derive_root_keypair_from_entropy, derive_sr25519_hard_path, + index_bytes, + }, +}; +use crate::runtime::statement_allowance::{ + inspection::{ + AccountLabels, ActivationGuard, AllowanceSection, ProductAccounts, ReadOnlyChain, + WalletAllowanceSnapshot, + }, + rpc::RpcClient, +}; + +const MAX_PRODUCT_IDS: usize = 32; + +fn error(reason: impl ToString) -> GenericError { + GenericError { + reason: reason.to_string(), + } +} + +fn validate_products(product_ids: &[String]) -> Result<(), GenericError> { + if product_ids.len() > MAX_PRODUCT_IDS { + return Err(error( + "wallet allowance inspection accepts at most 32 product IDs", + )); + } + let mut seen = HashSet::with_capacity(product_ids.len()); + for product in product_ids { + let normalized = normalize_product_identifier(product).map_err(error)?; + if normalized != *product || !seen.insert(product) { + return Err(error( + "wallet allowance product IDs must be unique and canonical", + )); + } + } + Ok(()) +} + +impl SigningHost { + /// Read real capacity and current occupancy, without refresh, proof, claims, + /// registration, renewal, signing, or a write to local storage. + pub(crate) async fn get_wallet_allowance_snapshot( + &self, + activation_id: &str, + product_ids: Vec, + ) -> Result { + self.check_identity_activation(activation_id)?; + validate_products(&product_ids)?; + let context = self.local_identity_context()?; + let session = self + .current_local_session() + .ok_or_else(|| error("no active local identity"))?; + let candidates = self + .reserved_person_collection_candidates(&session) + .map_err(error)?; + let entropy = self.root_entropy().map_err(error)?; + let root = derive_root_keypair_from_entropy(&entropy).map_err(error)?; + let owner = root.public.to_bytes(); + if owner != session.public_key { + return Err(error("local identity activation changed")); + } + let mut known_labels = AccountLabels::new(); + let identity = self.identity_keypair().map_err(error)?.public.to_bytes(); + known_labels.insert(identity, (None, "Wallet identity / SSO".to_string())); + let mut products = Vec::with_capacity(product_ids.len()); + for product_id in &product_ids { + let pgas = derive_product_keypair(&root, product_id, index_bytes(0)) + .map_err(error)? + .public + .to_bytes(); + let bulletin = + derive_sr25519_hard_path(&entropy, &["allowance", "bulletin", product_id]) + .map_err(error)? + .public + .to_bytes(); + let statement = + derive_sr25519_hard_path(&entropy, &["allowance", "statement-store", product_id]) + .map_err(error)? + .public + .to_bytes(); + known_labels.insert( + statement, + (Some(product_id.clone()), format!("Product: {product_id}")), + ); + products.push(ProductAccounts { + product_id: product_id.clone(), + pgas, + bulletin, + }); + } + drop(root); + self.check_identity_activation(activation_id)?; + let guard = || { + self.check_identity_activation(activation_id) + .map_err(|error| error.reason) + }; + // The ledger is read strictly. A broken ledger cannot be silently + // represented as no known device/product attribution. + guard().map_err(error)?; + let labels = allowance_renewal::inspection_labels(self, &entropy, owner).await; + guard().map_err(error)?; + drop(entropy); + let labels = labels.map(|mut labels| { + labels.extend(known_labels); + labels + }); + + // Each chain pins one independent finalized block and fetches metadata, + // policies, storage and clock from that block. No shared best-head cache. + let (people, asset_hub, bulletin) = futures::join!( + self.allowance_people(&guard), + self.allowance_asset_hub(&guard), + self.allowance_bulletin(&guard), + ); + guard().map_err(error)?; + let memberships = match &people { + Ok(chain) => chain.memberships(&candidates).await, + Err(reason) => Err(reason.clone()), + }; + guard().map_err(error)?; + let suffix = self.network_suffix.as_bytes(); + let (statement_store, pgas_claims, pgas_balances, bulletin_claims, bulletin_quotas) = futures::join!( + async { + match (&people, &memberships, &labels) { + (Ok(chain), Ok(memberships), Ok(labels)) => chain.section( + chain + .statement(&candidates, memberships, suffix, labels) + .await, + ), + (Err(reason), _, _) | (_, Err(reason), _) | (_, _, Err(reason)) => { + AllowanceSection::unavailable(reason) + } + } + }, + async { + match (&asset_hub, &people, &memberships) { + (Ok(chain), Ok(people), Ok(memberships)) => chain.section( + chain + .pgas_claims(&candidates, memberships, &people.observation, suffix) + .await, + ), + (Err(reason), _, _) | (_, Err(reason), _) | (_, _, Err(reason)) => { + AllowanceSection::unavailable(reason) + } + } + }, + async { + match &asset_hub { + Ok(chain) => chain.section(chain.pgas_balances(&products).await), + Err(reason) => AllowanceSection::unavailable(reason), + } + }, + async { + match (&people, &memberships) { + (Ok(chain), Ok(memberships)) => chain.section( + chain + .bulletin_claims(&candidates, memberships, suffix) + .await, + ), + (Err(reason), _) | (_, Err(reason)) => AllowanceSection::unavailable(reason), + } + }, + async { + match &bulletin { + Ok(chain) => chain.section(chain.bulletin_quotas(&products).await), + Err(reason) => AllowanceSection::unavailable(reason), + } + }, + ); + // A stale activation is a failed request, never a partly-unavailable + // response which might still reveal the previous identity's sections. + guard().map_err(error)?; + Ok(WalletAllowanceSnapshot { + schema_version: 1, + identity_account_id: context.identity_account_id, + network_suffix: self.network_suffix.clone(), + product_ids, + statement_store, + pgas_claims, + pgas_balances, + bulletin_claims, + bulletin_quotas, + }) + } + + async fn allowance_people<'a>( + &self, + guard: ActivationGuard<'a>, + ) -> Result, String> { + guard()?; + let client = self + .services + .statement_store + .chain_client("wallet allowance inspection") + .await; + guard()?; + ReadOnlyChain::open_client(client.map_err(|error| error.to_string())?, guard).await + } + + async fn allowance_asset_hub<'a>( + &self, + guard: ActivationGuard<'a>, + ) -> Result, String> { + guard()?; + let chains = features::supported_chains(self.platform.as_ref()).await; + guard()?; + let genesis = features::genesis_for( + &chains.map_err(|error| error.reason)?, + ChainIdentifier::AssetHub, + ) + .ok_or_else(|| "Asset Hub is not configured".to_string())?; + let client = self + .services + .chain + .rpc_client("wallet allowance inspection", &genesis) + .await; + guard()?; + let rpc = RpcClient::new(subxt_rpcs::RpcClient::new( + client.map_err(|error| error.to_string())?, + )); + ReadOnlyChain::open(rpc, genesis, guard).await + } + + async fn allowance_bulletin<'a>( + &self, + guard: ActivationGuard<'a>, + ) -> Result, String> { + guard()?; + let genesis = self.services.bulletin.genesis_hash(); + if genesis == [0; 32] { + return Err("Bulletin is not configured".to_string()); + } + let client = self + .services + .bulletin + .client("wallet allowance inspection") + .await; + guard()?; + ReadOnlyChain::open( + RpcClient::new(client.map_err(|error| error.to_string())?), + genesis, + guard, + ) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn trusted_hints_reject_duplicates_noncanonical_and_unbounded_input() { + assert!(validate_products(&["app.dot".to_string(), "app.dot".to_string()]).is_err()); + assert!(validate_products(&[" App.DOT ".to_string()]).is_err()); + assert!(validate_products(&["".to_string()]).is_err()); + let too_many = (0..=MAX_PRODUCT_IDS) + .map(|index| format!("app{index}.dot")) + .collect::>(); + assert!(validate_products(&too_many).is_err()); + } +} diff --git a/rust/crates/truapi-server/src/runtime/statement_allowance.rs b/rust/crates/truapi-server/src/runtime/statement_allowance.rs index 3ed92e535..a415aeed6 100644 --- a/rust/crates/truapi-server/src/runtime/statement_allowance.rs +++ b/rust/crates/truapi-server/src/runtime/statement_allowance.rs @@ -1,14 +1,19 @@ //! On-chain statement-store allowance registration (`set_statement_store_account`). //! -//! Mirrors how an iOS/web client obtains statement-store allowance from the real -//! People chain: build the `Resources.set_statement_store_account` call, prove -//! personhood ring membership with the caller's registry-selected ring-VRF key, -//! and submit the resulting unsigned General (v5) extrinsic. Native only -//! (needs the `verifiable` prover and live chain reads). +//! Mirrors how iOS and browser account holders obtain statement-store +//! allowance from the real People chain: build the +//! `Resources.set_statement_store_account` call, prove personhood ring +//! membership with the caller's registry-selected ring-VRF key, and submit the +//! resulting unsigned General (v5) extrinsic. +//! +//! These helpers accept host-backed RPC clients on native and browser targets; +//! the host supplies the chain connections and controls which claims it offers. +//! Opening a direct RPC URL is only available on native targets. pub mod collection; pub mod extension; pub mod extrinsic; +pub mod inspection; pub mod pgas; pub mod proof; pub mod renewal; @@ -24,8 +29,6 @@ use std::sync::{Arc, Mutex}; use std::time::Duration; #[cfg(not(target_arch = "wasm32"))] use std::time::Instant; -#[cfg(target_arch = "wasm32")] -use web_time::Instant; use futures::FutureExt; use parity_scale_codec::{Decode, Encode}; @@ -33,6 +36,8 @@ use serde_json::{Value, json}; use sp_crypto_hashing::twox_128; use thiserror::Error; use tracing::{debug, warn}; +#[cfg(target_arch = "wasm32")] +use web_time::Instant; use collection::PersonhoodCollection; use extension::{ChainState, Metadata, MetadataError}; diff --git a/rust/crates/truapi-server/src/runtime/statement_allowance/extension.rs b/rust/crates/truapi-server/src/runtime/statement_allowance/extension.rs index 599f75981..b9a0705aa 100644 --- a/rust/crates/truapi-server/src/runtime/statement_allowance/extension.rs +++ b/rust/crates/truapi-server/src/runtime/statement_allowance/extension.rs @@ -200,7 +200,7 @@ pub struct Metadata { extension_version: u8, registry: PortableRegistry, storage_values: HashMap<(String, String), u32>, - constants: HashMap<(String, String), Vec>, + constants: HashMap<(String, String), (Vec, u32)>, calls: HashMap, view_functions: HashMap<(String, String), ViewFunctionDef>, view_values: Mutex>, @@ -260,7 +260,7 @@ macro_rules! collect_pallets { for constant in &pallet.constants { constants.insert( (pallet.name.clone(), constant.name.clone()), - constant.value.clone(), + (constant.value.clone(), constant.ty.id), ); } let Some(storage) = &pallet.storage else { @@ -432,7 +432,14 @@ impl Metadata { pub fn constant(&self, pallet: &str, name: &str) -> Option<&[u8]> { self.constants .get(&(pallet.to_string(), name.to_string())) - .map(Vec::as_slice) + .map(|(bytes, _)| bytes.as_slice()) + } + + /// Declared constant type, for strict metadata-aware inspection decoding. + pub(super) fn constant_type(&self, pallet: &str, name: &str) -> Option { + self.constants + .get(&(pallet.to_string(), name.to_string())) + .map(|(_, type_id)| *type_id) } pub(super) fn view_function(&self, pallet: &str, function: &str) -> Option { diff --git a/rust/crates/truapi-server/src/runtime/statement_allowance/inspection.rs b/rust/crates/truapi-server/src/runtime/statement_allowance/inspection.rs new file mode 100644 index 000000000..95154dc81 --- /dev/null +++ b/rust/crates/truapi-server/src/runtime/statement_allowance/inspection.rs @@ -0,0 +1,1210 @@ +//! Read-only, finalized wallet allowance observations. No allocation or proof submission. + +use std::collections::HashMap; + +use parity_scale_codec::{DecodeAll, Encode}; +use scale_decode::DecodeAsType; +use serde::Serialize; +use serde_json::{Value, json}; +use sp_crypto_hashing::twox_128; + +use super::{ + CollectionCandidate, collection::PersonhoodCollection, extension::Metadata, pgas, proof, ring, + rpc::RpcClient, slot, view, +}; + +const STORAGE_BATCH: usize = 32; +// Bound work even if a runtime advertises an unexpectedly large policy. +const MAX_INSPECTION_SLOTS: u32 = 4096; +const JS_MAX_INTEGER: u64 = 9_007_199_254_740_991; + +/// Finalized chain state used for one observation, not an atomic cross-chain time. +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AllowanceObservation { + /// Lowercase hex genesis hash identifying the configured chain. + pub genesis_hash: String, + /// Lowercase hex hash pinning metadata, storage, and runtime calls. + pub block_hash: String, + /// Height of the pinned finalized block. + pub block_number: u32, + /// Runtime version used to decode the pinned state. + pub spec_version: u32, + /// Pinned chain time in Unix seconds, never the browser clock. + pub chain_timestamp: u64, +} + +/// Independent resource result; unavailable data never implies zero capacity. +#[derive(Debug, Serialize)] +#[serde(tag = "status", rename_all = "lowercase")] +pub enum AllowanceSection { + /// A complete, decoded observation from the stated chain block. + Available { + /// Provenance for this section's chain-local values. + observation: AllowanceObservation, + /// Resource-specific quantities and policy. + value: T, + }, + /// No capacity can be established for this section. + Unavailable { + /// Diagnostic explaining the failed observation. + reason: String, + }, +} + +impl AllowanceSection { + pub(crate) fn unavailable(reason: impl ToString) -> Self { + Self::Unavailable { + reason: reason.to_string(), + } + } +} + +/// One occupied period slot; recipient attribution may not exist on chain. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AllowanceSlot { + /// Index in this collection's allowance-slot namespace. + pub index: u32, + /// Public recipient when the storage record exposes one. + #[serde(skip_serializing_if = "Option::is_none")] + pub account_id: Option, + /// Product identifier only when native derivation verifies the mapping. + #[serde(skip_serializing_if = "Option::is_none")] + pub product_id: Option, + /// Trusted local device or allocation description, if known. + #[serde(skip_serializing_if = "Option::is_none")] + pub label: Option, + /// On-chain assignment time in Unix seconds, where available. + #[serde(skip_serializing_if = "Option::is_none")] + pub since: Option, +} + +/// Collection-specific occupancy and usable capacity under native allocation policy. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AllowancePool { + /// Personhood collection: `People` or `LitePeople`. + pub collection: String, + /// Finalized membership result: `verified` or `not-found`. + pub membership: &'static str, + /// Whether the current native allocator selects this collection. + pub selected: bool, + /// Runtime policy limit, retained even for a nonmember. + pub limit: u32, + /// Observed occupied slots, including allocations surviving lost membership. + pub used: u32, + /// Saturated unused capacity; zero when membership is absent. + pub remaining: u32, + /// Occupied entries only; missing recipient fields do not mean unused slots. + pub slots: Vec, +} + +/// Period boundaries and distinct collection budgets; pools are not implicitly pooled. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AllowanceClaims { + /// Runtime allowance period containing the pinned chain time. + pub period: u32, + /// Next period boundary in Unix seconds. + pub resets_at: u64, + /// Separately observed collection budgets and allocator selection. + pub pools: Vec, +} + +/// Statement publishing assignments, not message counts or stored-byte usage. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StatementAllowanceSnapshot { + /// Collection budgets and occupied publishing slots for this period. + #[serde(flatten)] + pub claims: AllowanceClaims, + /// Runtime grace window in seconds. + pub grace_seconds: u32, + /// Runtime delay before an existing assignment may be replaced. + pub replacement_cooldown_seconds: u32, +} + +/// Asset Hub claim opportunities, separate from any recipient's PGAS balance. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PgasClaimsSnapshot { + /// Asset Hub period and claim-slot occupancy. + #[serde(flatten)] + pub claims: AllowanceClaims, + /// Runtime-configured PGAS asset identifier as a decimal integer. + pub asset_id: String, + /// Amount per claim in exact decimal base units. + pub claim_amount: String, + /// Separate finalized People block used to verify collection membership. + pub membership_observation: AllowanceObservation, +} + +/// Total asset balance for one explicitly scoped product account. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PgasBalance { + /// Canonical trusted-shell product hint. + pub product_id: String, + /// Native-derived public product account in lowercase hex. + pub account_id: String, + /// Product derivation index; the first inspector supports only zero. + pub derivation_index: u32, + /// Exact decimal base units, or unknown on a failed read; not spendability. + pub balance: Option, + /// Per-account failure explaining an unknown balance. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +/// PGAS balances and metadata read at the same finalized Asset Hub block. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PgasBalancesSnapshot { + /// Runtime-configured asset identifier as a decimal integer. + pub asset_id: String, + /// Asset metadata precision, absent when metadata cannot be established. + pub decimals: Option, + /// Asset metadata symbol, absent rather than guessed. + pub symbol: Option, + /// One result per requested product; other derivations are outside scope. + pub accounts: Vec, +} + +/// Bulletin authorization for a dedicated product storage key. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BulletinQuota { + /// Canonical trusted-shell product hint. + pub product_id: String, + /// Dedicated Bulletin allowance account, not the PGAS product account. + pub account_id: String, + /// `active`, `expired`, `missing`, or `unavailable` at the source block. + pub status: &'static str, + /// Raw charged-byte counter in exact decimal units; may exceed the quota. + #[serde(skip_serializing_if = "Option::is_none")] + pub bytes_used: Option, + /// Granted byte allowance in exact decimal units. + #[serde(skip_serializing_if = "Option::is_none")] + pub bytes_limit: Option, + /// Saturated byte remainder; not usable after authorization expiry. + #[serde(skip_serializing_if = "Option::is_none")] + pub bytes_remaining: Option, + /// Raw submission counter, which can exceed a soft quota. + #[serde(skip_serializing_if = "Option::is_none")] + pub transactions_used: Option, + /// Granted submission allowance. + #[serde(skip_serializing_if = "Option::is_none")] + pub transactions_limit: Option, + /// Saturated submission remainder; not usable after expiry. + #[serde(skip_serializing_if = "Option::is_none")] + pub transactions_remaining: Option, + /// First Bulletin block at which the authorization is expired. + #[serde(skip_serializing_if = "Option::is_none")] + pub expires_at_block: Option, + /// Per-account read or decode failure, distinct from missing authorization. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +/// Independently decoded Bulletin authorizations for the requested products. +#[derive(Debug, Serialize)] +pub struct BulletinQuotasSnapshot { + /// Complete requested scope, including missing or unavailable accounts. + pub accounts: Vec, +} + +/// Public, read-only wallet inspection bound to one native identity activation. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WalletAllowanceSnapshot { + /// DTO schema version, currently one. + pub schema_version: u32, + /// Native-derived identity account in lowercase hex; never key material. + pub identity_account_id: String, + /// Native network derivation suffix, not a display label. + pub network_suffix: String, + /// Validated canonical product hints defining balance and quota scope. + pub product_ids: Vec, + /// People-chain publishing-slot occupancy and policy. + pub statement_store: AllowanceSection, + /// Asset Hub claims with separately pinned People membership provenance. + pub pgas_claims: AllowanceSection, + /// Current product Index(0) balances, independent of claim availability. + pub pgas_balances: AllowanceSection, + /// People-chain storage-claim opportunities under native collection policy. + pub bulletin_claims: AllowanceSection, + /// Bulletin storage authorizations, byte/submission counters, and expiry. + pub bulletin_quotas: AllowanceSection, +} + +pub(crate) type ActivationGuard<'a> = &'a (dyn Fn() -> Result<(), String> + Sync); +pub(crate) type AccountLabels = HashMap<[u8; 32], (Option, String)>; + +/// Only public account identifiers, derived before any network request. +pub(crate) struct ProductAccounts { + pub product_id: String, + pub pgas: [u8; 32], + pub bulletin: [u8; 32], +} + +pub(crate) struct ReadOnlyChain<'a> { + rpc: RpcClient, + metadata: Metadata, + pub observation: AllowanceObservation, + guard: ActivationGuard<'a>, +} + +fn reason(error: impl ToString) -> String { + error.to_string() +} + +async fn checked_call( + rpc: &RpcClient, + guard: ActivationGuard<'_>, + method: &str, + params: Value, +) -> Result { + guard()?; + let result = rpc.call(method, params).await; + guard()?; + result.map_err(reason) +} + +fn hex_bytes(value: &Value) -> Result, String> { + let text = value + .as_str() + .and_then(|s| s.strip_prefix("0x")) + .ok_or_else(|| "RPC result is not 0x-prefixed hex".to_string())?; + hex::decode(text).map_err(reason) +} + +fn hash(value: &Value) -> Result { + let bytes = hex_bytes(value)?; + if bytes.len() != 32 { + return Err("RPC block hash is not 32 bytes".to_string()); + } + Ok(format!("0x{}", hex::encode(bytes))) +} + +fn decode_all(bytes: &[u8], context: &str) -> Result { + T::decode_all(&mut &bytes[..]).map_err(|error| format!("{context}: {error}")) +} + +fn decode_metadata(bytes: &[u8]) -> Result { + // Metadata::decode is also used by allocators. Keep its compatibility + // behaviour there, but inspection must reject trailing/incomplete data. + let _: frame_metadata::RuntimeMetadataPrefixed = decode_all(bytes, "metadata")?; + Metadata::decode(bytes).map_err(reason) +} + +impl<'a> ReadOnlyChain<'a> { + pub(crate) async fn open_client( + client: super::ChainClient, + guard: ActivationGuard<'a>, + ) -> Result { + Self::open(client.rpc, client.configured_genesis_hash, guard).await + } + + pub(crate) async fn open( + rpc: RpcClient, + expected_genesis: [u8; 32], + guard: ActivationGuard<'a>, + ) -> Result { + let genesis = hash(&checked_call(&rpc, guard, "chain_getBlockHash", json!([0])).await?)?; + if genesis != format!("0x{}", hex::encode(expected_genesis)) { + return Err("RPC genesis differs from the configured chain".to_string()); + } + let at = hash(&checked_call(&rpc, guard, "chain_getFinalizedHead", json!([])).await?)?; + let header = checked_call(&rpc, guard, "chain_getHeader", json!([at])).await?; + let number = header + .get("number") + .and_then(Value::as_str) + .and_then(|s| s.strip_prefix("0x")) + .ok_or_else(|| "finalized header has no hex block number".to_string())?; + let block_number = u32::from_str_radix(number, 16).map_err(reason)?; + let runtime = checked_call(&rpc, guard, "state_getRuntimeVersion", json!([at])).await?; + let spec_version = runtime + .get("specVersion") + .and_then(Value::as_u64) + .and_then(|n| u32::try_from(n).ok()) + .ok_or_else(|| "runtime has no valid specVersion".to_string())?; + let versioned = checked_call( + &rpc, + guard, + "state_call", + json!([ + "Metadata_metadata_at_version", + format!("0x{}", hex::encode(16u32.encode())), + at + ]), + ) + .await; + let metadata = match versioned { + Ok(value) => { + let bytes = hex_bytes(&value)?; + match decode_all::>>(&bytes, "Metadata_metadata_at_version")? { + Some(bytes) => decode_metadata(&bytes)?, + None => Self::legacy_metadata(&rpc, guard, &at).await?, + } + } + // Older runtimes do not implement the metadata runtime API. The + // fallback still names this exact block; malformed success is never + // accepted or reinterpreted as absence. + Err(_) => Self::legacy_metadata(&rpc, guard, &at).await?, + }; + let mut chain = Self { + rpc, + metadata, + guard, + observation: AllowanceObservation { + genesis_hash: genesis, + block_hash: at, + block_number, + spec_version, + chain_timestamp: 0, + }, + }; + let bytes = chain + .storage(slot::timestamp_now_key()) + .await? + .ok_or_else(|| "Timestamp.Now is absent at the finalized block".to_string())?; + chain.observation.chain_timestamp = + chain.decode_storage::("Timestamp", "Now", &bytes)? / 1000; + if chain.observation.chain_timestamp > JS_MAX_INTEGER { + return Err("chain timestamp exceeds the snapshot integer range".to_string()); + } + Ok(chain) + } + + async fn legacy_metadata( + rpc: &RpcClient, + guard: ActivationGuard<'_>, + at: &str, + ) -> Result { + let bytes = hex_bytes(&checked_call(rpc, guard, "state_getMetadata", json!([at])).await?)?; + if bytes.starts_with(b"meta") { + decode_metadata(&bytes) + } else { + decode_metadata(&decode_all::>(&bytes, "opaque metadata")?) + } + } + + pub(crate) fn section(&self, result: Result) -> AllowanceSection { + match result { + Ok(value) => AllowanceSection::Available { + observation: self.observation.clone(), + value, + }, + Err(reason) => AllowanceSection::Unavailable { reason }, + } + } + + async fn storage(&self, key: Vec) -> Result>, String> { + Ok(self.storage_many(&[key]).await?.remove(0)) + } + + async fn storage_many(&self, keys: &[Vec]) -> Result>>, String> { + let mut values = Vec::with_capacity(keys.len()); + for chunk in keys.chunks(STORAGE_BATCH) { + (self.guard)()?; + let result = self + .rpc + .get_storage_many_at(chunk, &self.observation.block_hash) + .await; + (self.guard)()?; + values.extend(result.map_err(reason)?); + } + Ok(values) + } + + fn storage_type(&self, pallet: &str, entry: &str) -> Result { + self.metadata + .storage_value_type(pallet, entry) + .ok_or_else(|| format!("runtime does not expose {pallet}.{entry}")) + } + + fn decode_storage( + &self, + pallet: &str, + entry: &str, + bytes: &[u8], + ) -> Result { + let mut input = bytes; + let value = T::decode_as_type( + &mut input, + self.storage_type(pallet, entry)?, + self.metadata.registry(), + ) + .map_err(|error| format!("{pallet}.{entry}: {error}"))?; + if !input.is_empty() { + return Err(format!("{pallet}.{entry}: trailing storage bytes")); + } + Ok(value) + } + + fn constant(&self, pallet: &str, name: &str) -> Result { + let bytes = self + .metadata + .constant(pallet, name) + .ok_or_else(|| format!("runtime does not expose {pallet}.{name}"))?; + let type_id = self + .metadata + .constant_type(pallet, name) + .ok_or_else(|| format!("runtime does not expose the type of {pallet}.{name}"))?; + let mut input = bytes; + let value = T::decode_as_type(&mut input, type_id, self.metadata.registry()) + .map_err(|error| format!("{pallet}.{name}: {error}"))?; + if !input.is_empty() { + return Err(format!("{pallet}.{name}: trailing constant bytes")); + } + Ok(value) + } + + async fn policy(&self, name: &'static str) -> Result { + (self.guard)()?; + let result = view::read_resource_u32_at( + &self.rpc, + &self.metadata, + name, + &self.observation.block_hash, + ) + .await; + (self.guard)()?; + result.map_err(reason) + } + + async fn require_suffix(&self, expected: &[u8]) -> Result<(), String> { + let key = [ + twox_128(b"NetworkSuffix").as_slice(), + twox_128(b"NetworkSuffix").as_slice(), + ] + .concat(); + let bytes = self + .storage(key) + .await? + .ok_or_else(|| "NetworkSuffix is absent".to_string())?; + let actual: Vec = decode_all(&bytes, "NetworkSuffix")?; + if actual != expected { + return Err("chain NetworkSuffix differs from the active wallet".to_string()); + } + Ok(()) + } + + /// Match the allocator's newest-to-oldest baked-ring search. A member's + /// current record can point at an unbaked ring while an older proof works. + pub(crate) async fn memberships( + &self, + candidates: &[CollectionCandidate], + ) -> Result, String> { + self.storage_type("Members", "Collections")?; + self.storage_type("Members", "CurrentRingIndex")?; + self.storage_type("Members", "RingKeysStatus")?; + self.storage_type("Members", "RingKeys")?; + let mut result = Vec::with_capacity(candidates.len()); + for candidate in candidates { + let values = self + .storage_many(&[ + ring::collections_key(candidate.collection), + ring::current_ring_index_key(candidate.collection), + ]) + .await?; + let Some(info) = &values[0] else { + result.push(false); + continue; + }; + let info: ring::CollectionInfo = self.decode_storage("Members", "Collections", info)?; + let exponent = info.ring_size.exponent(); + proof::domain_for_ring_exponent(exponent).map_err(reason)?; + let current = values[1] + .as_deref() + .map(|bytes| self.decode_storage("Members", "CurrentRingIndex", bytes)) + .transpose()? + .unwrap_or(0); + result.push( + self.has_including_ring( + candidate.collection, + &proof::member_key(candidate.entropy), + exponent, + current, + ) + .await?, + ); + } + Ok(result) + } + + async fn has_including_ring( + &self, + collection: PersonhoodCollection, + member: &[u8; 32], + exponent: u8, + current: u32, + ) -> Result { + let capacity = 1u32 << exponent; + for ring_index in (0..=current).rev() { + let status = self + .storage(ring::ring_keys_status_key(collection, ring_index)) + .await? + .map(|bytes| self.decode_storage::("Members", "RingKeysStatus", &bytes)) + .transpose()?; + if status + .as_ref() + .is_some_and(|status| status.included > status.total || status.total > capacity) + { + return Err("ring included/total exceeds its domain".to_string()); + } + // The allocator treats a missing status as an entirely baked ring. + let total = status.as_ref().map(|status| status.total); + let included = status.as_ref().map_or(capacity, |status| status.included); + let mut count = 0u32; + let mut found = false; + for page in 0..=capacity { + if total == Some(count) { + break; + } + let value = self + .storage(ring::ring_keys_key(collection, ring_index, page)) + .await?; + let Some(bytes) = value else { break }; + let members: Vec<[u8; 32]> = decode_all(&bytes, "Members.RingKeys")?; + if members.is_empty() { + break; + } + for key in members { + if count >= total.unwrap_or(capacity) { + return Err("ring key pages exceed total".to_string()); + } + found |= count < included && &key == member; + count += 1; + } + } + if total.is_some_and(|total| count != total) { + return Err("ring key pages are incomplete".to_string()); + } + if found { + return Ok(true); + } + } + Ok(false) + } + + pub(crate) async fn statement( + &self, + candidates: &[CollectionCandidate], + memberships: &[bool], + suffix: &[u8], + labels: &AccountLabels, + ) -> Result { + self.require_suffix(suffix).await?; + self.storage_type("Resources", "StatementStoreAllowances")?; + let (period, resets_at) = period( + self.observation.chain_timestamp, + slot::STATEMENT_STORE_PERIOD_SECONDS, + )?; + let grace_seconds = self.policy("get_stmt_store_grace_window").await?; + let replacement_cooldown_seconds = + self.policy("get_stmt_store_replacement_cooldown").await?; + let mut pools = Vec::with_capacity(candidates.len()); + for (candidate, &member) in candidates.iter().zip(memberships) { + let limit = bounded_limit( + self.policy(candidate.collection.slots_per_period_view()) + .await?, + )?; + let mut slots = Vec::new(); + for start in (0..limit).step_by(STORAGE_BATCH) { + (self.guard)()?; + let end = (start + STORAGE_BATCH as u32).min(limit); + let keys = (start..end) + .map(|index| { + slot::slot_alias(candidate.entropy, suffix, period, index) + .map(|alias| slot::statement_store_allowance_key(period, &alias)) + .map_err(reason) + }) + .collect::, _>>()?; + for (index, bytes) in (start..end).zip(self.storage_many(&keys).await?) { + let Some(bytes) = bytes else { + continue; + }; + let entry: slot::StatementStoreAllowanceEntry = + self.decode_storage("Resources", "StatementStoreAllowances", &bytes)?; + if entry.seq != index || entry.since > self.observation.chain_timestamp { + return Err("statement slot index or timestamp contradicts its snapshot" + .to_string()); + } + let label = labels.get(&entry.account_id); + slots.push(AllowanceSlot { + index, + account_id: Some(account_hex(&entry.account_id)), + since: Some(entry.since), + product_id: label.and_then(|label| label.0.clone()), + label: label.map(|label| label.1.clone()), + }); + } + } + pools.push(pool(candidate.collection, member, member, limit, slots)?); + } + Ok(StatementAllowanceSnapshot { + claims: AllowanceClaims { + period, + resets_at, + pools, + }, + grace_seconds, + replacement_cooldown_seconds, + }) + } + + async fn claimed_slots( + &self, + candidate: &CollectionCandidate, + suffix: &[u8], + period: u32, + limit: u32, + bulletin: bool, + ) -> Result, String> { + let limit = bounded_limit(limit)?; + let (pallet, entry) = if bulletin { + ("Resources", "SpentLongTermStorageAliases") + } else { + ("Pgas", "ClaimedGasAliases") + }; + self.storage_type(pallet, entry)?; + let mut slots = Vec::new(); + for start in (0..limit).step_by(STORAGE_BATCH) { + (self.guard)()?; + let end = (start + STORAGE_BATCH as u32).min(limit); + let keys = (start..end) + .map(|index| { + if bulletin { + let counter = u8::try_from(index).map_err(reason)?; + let alias = slot::long_term_storage_alias( + candidate.entropy, + suffix, + period, + counter, + ) + .map_err(reason)?; + Ok(slot::spent_long_term_storage_alias_key(period, &alias)) + } else { + let alias = slot::pgas_alias(candidate.entropy, suffix, period, index) + .map_err(reason)?; + Ok(slot::claimed_gas_alias_key(period, &alias)) + } + }) + .collect::, String>>()?; + for (index, bytes) in (start..end).zip(self.storage_many(&keys).await?) { + if let Some(bytes) = bytes { + self.decode_storage::<()>(pallet, entry, &bytes)?; + slots.push(AllowanceSlot { + index, + account_id: None, + product_id: None, + label: None, + since: None, + }); + } + } + } + Ok(slots) + } + + pub(crate) async fn pgas_claims( + &self, + candidates: &[CollectionCandidate], + memberships: &[bool], + membership_observation: &AllowanceObservation, + suffix: &[u8], + ) -> Result { + self.require_suffix(suffix).await?; + let asset_id: u32 = self.constant("Pgas", "PgasAssetId")?; + let claim_amount: u128 = self.constant("Pgas", "PgasClaimAmount")?; + let (period, resets_at) = period( + self.observation.chain_timestamp, + slot::STATEMENT_STORE_PERIOD_SECONDS, + )?; + let selected = memberships.iter().position(|member| *member); + let mut pools = Vec::with_capacity(candidates.len()); + for (index, (candidate, &member)) in candidates.iter().zip(memberships).enumerate() { + let limit: u32 = self.constant( + "Pgas", + candidate.collection.pgas_claims_per_period_constant(), + )?; + let slots = self + .claimed_slots(candidate, suffix, period, limit, false) + .await?; + pools.push(pool( + candidate.collection, + member, + selected == Some(index), + limit, + slots, + )?); + } + Ok(PgasClaimsSnapshot { + claims: AllowanceClaims { + period, + resets_at, + pools, + }, + asset_id: asset_id.to_string(), + claim_amount: claim_amount.to_string(), + membership_observation: membership_observation.clone(), + }) + } + + pub(crate) async fn bulletin_claims( + &self, + candidates: &[CollectionCandidate], + memberships: &[bool], + suffix: &[u8], + ) -> Result { + self.require_suffix(suffix).await?; + let duration: u32 = self.constant("Resources", "LongTermStoragePeriodDuration")?; + let (period, resets_at) = period(self.observation.chain_timestamp, u64::from(duration))?; + let limit = self + .policy("get_long_term_storage_claims_per_period") + .await?; + u8::try_from(limit) + .map_err(|_| "Bulletin claim limit exceeds the native counter width".to_string())?; + let selected = candidates + .iter() + .zip(memberships) + .position(|(c, m)| *m && c.collection == PersonhoodCollection::LitePeople) + .or_else(|| memberships.iter().position(|member| *member)); + let mut pools = Vec::with_capacity(candidates.len()); + for (index, (candidate, &member)) in candidates.iter().zip(memberships).enumerate() { + let slots = self + .claimed_slots(candidate, suffix, period, limit, true) + .await?; + pools.push(pool( + candidate.collection, + member, + selected == Some(index), + limit, + slots, + )?); + } + Ok(AllowanceClaims { + period, + resets_at, + pools, + }) + } + + pub(crate) async fn pgas_balances( + &self, + products: &[ProductAccounts], + ) -> Result { + let asset_id: u32 = self.constant("Pgas", "PgasAssetId")?; + self.storage_type("Assets", "Metadata")?; + self.storage_type("Assets", "Account")?; + let key = [ + twox_128(b"Assets").as_slice(), + twox_128(b"Metadata").as_slice(), + &ring::blake2_128_concat(&asset_id.to_le_bytes()), + ] + .concat(); + let metadata = self.storage(key).await?; + let (decimals, symbol) = match metadata { + Some(bytes) => { + let value: AssetMetadata = self.decode_storage("Assets", "Metadata", &bytes)?; + ( + Some(value.decimals), + String::from_utf8(value.symbol) + .ok() + .filter(|symbol| !symbol.is_empty()), + ) + } + None => (None, None), + }; + let keys = products + .iter() + .map(|p| pgas::pgas_balance_key(asset_id, &p.pgas)) + .collect::>(); + let values = self.storage_many(&keys).await?; + let accounts = products + .iter() + .zip(values) + .map(|(product, value)| { + let balance = match value { + Some(bytes) => self + .decode_storage::("Assets", "Account", &bytes) + .map(|a| a.balance.to_string()), + None => Ok("0".to_string()), + }; + let (balance, error) = match balance { + Ok(balance) => (Some(balance), None), + Err(error) => (None, Some(error)), + }; + PgasBalance { + product_id: product.product_id.clone(), + account_id: account_hex(&product.pgas), + derivation_index: 0, + balance, + error, + } + }) + .collect(); + Ok(PgasBalancesSnapshot { + asset_id: asset_id.to_string(), + decimals, + symbol, + accounts, + }) + } + + pub(crate) async fn bulletin_quotas( + &self, + products: &[ProductAccounts], + ) -> Result { + self.storage_type("TransactionStorage", "Authorizations")?; + let keys = products + .iter() + .map(|p| super::bulletin_authorization_key(&p.bulletin)) + .collect::>(); + let values = self.storage_many(&keys).await?; + let accounts = products + .iter() + .zip(values) + .map(|(product, bytes)| { + let mut quota = BulletinQuota { + product_id: product.product_id.clone(), + account_id: account_hex(&product.bulletin), + status: "missing", + bytes_used: None, + bytes_limit: None, + bytes_remaining: None, + transactions_used: None, + transactions_limit: None, + transactions_remaining: None, + expires_at_block: None, + error: None, + }; + if let Some(bytes) = bytes { + let result = self + .decode_storage::( + "TransactionStorage", + "Authorizations", + &bytes, + ) + .map(|value| value.apply(&mut quota, self.observation.block_number)); + if let Err(error) = result { + quota.status = "unavailable"; + quota.error = Some(error); + } + } + quota + }) + .collect(); + Ok(BulletinQuotasSnapshot { accounts }) + } +} + +#[derive(DecodeAsType)] +struct RingStatus { + total: u32, + included: u32, +} +#[derive(DecodeAsType)] +struct AssetMetadata { + decimals: u8, + symbol: Vec, +} +#[derive(DecodeAsType)] +struct AssetAccount { + balance: u128, +} +#[derive(DecodeAsType)] +struct BulletinAuthorization { + extent: BulletinExtent, + expiration: u32, +} + +#[derive(DecodeAsType)] +struct BulletinExtent { + transactions: u32, + transactions_allowance: u32, + bytes: u64, + bytes_allowance: u64, +} + +impl BulletinAuthorization { + fn apply(self, quota: &mut BulletinQuota, at: u32) { + // These counters are soft consumption signals, not hard validity caps. + // Preserve over-limit usage; the native allocator saturates remaining. + let extent = self.extent; + quota.status = if at >= self.expiration { + "expired" + } else { + "active" + }; + quota.bytes_used = Some(extent.bytes.to_string()); + quota.bytes_limit = Some(extent.bytes_allowance.to_string()); + quota.bytes_remaining = Some( + extent + .bytes_allowance + .saturating_sub(extent.bytes) + .to_string(), + ); + quota.transactions_used = Some(extent.transactions); + quota.transactions_limit = Some(extent.transactions_allowance); + quota.transactions_remaining = Some( + extent + .transactions_allowance + .saturating_sub(extent.transactions), + ); + quota.expires_at_block = Some(self.expiration); + } +} + +pub(crate) fn account_hex(account: &[u8; 32]) -> String { + format!("0x{}", hex::encode(account)) +} + +fn period(now: u64, duration: u64) -> Result<(u32, u64), String> { + if duration == 0 { + return Err("allowance period duration is zero".to_string()); + } + let period = u32::try_from(now / duration).map_err(reason)?; + let resets_at = (u64::from(period) + 1) + .checked_mul(duration) + .filter(|value| *value <= JS_MAX_INTEGER) + .ok_or_else(|| "allowance reset timestamp overflow".to_string())?; + Ok((period, resets_at)) +} + +fn bounded_limit(limit: u32) -> Result { + if limit > MAX_INSPECTION_SLOTS { + return Err(format!( + "runtime allowance limit {limit} exceeds the bounded inspection limit" + )); + } + Ok(limit) +} + +fn pool( + collection: PersonhoodCollection, + member: bool, + selected: bool, + limit: u32, + slots: Vec, +) -> Result { + let used = u32::try_from(slots.len()).map_err(reason)?; + let remaining = limit + .checked_sub(used) + .ok_or_else(|| "occupied slots exceed the runtime limit".to_string())?; + Ok(AllowancePool { + collection: collection.metadata_variant().to_string(), + membership: if member { "verified" } else { "not-found" }, + selected: member && selected, + limit, + used, + remaining: if member { remaining } else { 0 }, + slots, + }) +} + +#[cfg(test)] +mod tests { + use super::super::rpc::testing::ScriptedRpc; + use super::*; + use subxt_rpcs::RpcClient as HostRpcClient; + + fn empty_quota() -> BulletinQuota { + BulletinQuota { + product_id: "app.dot".to_string(), + account_id: account_hex(&[1; 32]), + status: "missing", + bytes_used: None, + bytes_limit: None, + bytes_remaining: None, + transactions_used: None, + transactions_limit: None, + transactions_remaining: None, + expires_at_block: None, + error: None, + } + } + + #[test] + fn quota_expiry_is_the_block_boundary_and_soft_overuse_is_preserved() { + let authorization = || BulletinAuthorization { + extent: BulletinExtent { + transactions: 3, + transactions_allowance: 5, + bytes: 10, + bytes_allowance: 15, + }, + expiration: 20, + }; + let mut quota = empty_quota(); + authorization().apply(&mut quota, 19); + assert_eq!(quota.status, "active"); + assert_eq!(quota.bytes_remaining.as_deref(), Some("5")); + assert_eq!(quota.transactions_remaining, Some(2)); + authorization().apply(&mut quota, 20); + assert_eq!(quota.status, "expired"); + let mut excessive = authorization(); + excessive.extent.bytes = 16; + excessive.extent.transactions = 6; + excessive.apply(&mut quota, 19); + assert_eq!(quota.bytes_used.as_deref(), Some("16")); + assert_eq!(quota.bytes_remaining.as_deref(), Some("0")); + assert_eq!(quota.transactions_used, Some(6)); + assert_eq!(quota.transactions_remaining, Some(0)); + } + + #[test] + fn bulletin_storage_uses_its_metadata_extent_and_rejects_truncation() { + fn active() -> Result<(), String> { + Ok(()) + } + let fixture = include_bytes!("../../../tests/fixtures/bulletin_paseo_metadata.scale"); + let chain = ReadOnlyChain { + rpc: RpcClient::new(HostRpcClient::new(ScriptedRpc::new( + std::iter::empty::<&str>(), + ))), + metadata: decode_metadata(fixture).unwrap(), + observation: AllowanceObservation { + genesis_hash: account_hex(&[0; 32]), + block_hash: account_hex(&[1; 32]), + block_number: 19, + spec_version: 1, + chain_timestamp: 0, + }, + guard: &active, + }; + let bytes = (3u32, 5u32, 10u64, 0u64, 15u64, 20u32).encode(); + let authorization: BulletinAuthorization = chain + .decode_storage("TransactionStorage", "Authorizations", &bytes) + .unwrap(); + let mut quota = empty_quota(); + authorization.apply(&mut quota, 19); + assert_eq!(quota.bytes_remaining.as_deref(), Some("5")); + assert_eq!(quota.transactions_remaining, Some(2)); + assert!( + chain + .decode_storage::( + "TransactionStorage", + "Authorizations", + &bytes[..bytes.len() - 1] + ) + .is_err() + ); + let mut trailing = bytes; + trailing.push(0); + assert!( + chain + .decode_storage::( + "TransactionStorage", + "Authorizations", + &trailing + ) + .is_err() + ); + } + + #[test] + fn a_lost_membership_preserves_occupancy_but_never_advertises_capacity() { + let slot = AllowanceSlot { + index: 0, + account_id: Some(account_hex(&[1; 32])), + product_id: None, + label: None, + since: Some(7), + }; + let result = pool(PersonhoodCollection::People, false, true, 10, vec![slot]).unwrap(); + assert_eq!( + (result.limit, result.used, result.remaining, result.selected), + (10, 1, 0, false) + ); + } + + #[test] + fn periods_reject_zero_duration_and_unrepresentable_resets() { + assert_eq!(period(86_399, 86_400).unwrap(), (0, 86_400)); + assert_eq!(period(86_400, 86_400).unwrap(), (1, 172_800)); + assert!(period(1, 0).is_err()); + assert!(period(u64::MAX, u64::MAX).is_err()); + } + + #[test] + fn stale_activation_is_rejected_after_a_successful_rpc_read() { + use std::sync::atomic::{AtomicUsize, Ordering}; + let checks = AtomicUsize::new(0); + let guard = || { + if checks.fetch_add(1, Ordering::SeqCst) == 0 { + Ok(()) + } else { + Err("activation changed".to_string()) + } + }; + let scripted = ScriptedRpc::new(["null"]); + let rpc = RpcClient::new(HostRpcClient::new(scripted)); + let result = + futures::executor::block_on(checked_call(&rpc, &guard, "chain_getHeader", json!([]))); + assert_eq!(result.unwrap_err(), "activation changed"); + } + + #[test] + fn membership_can_use_an_older_baked_ring_when_the_current_ring_is_unbaked() { + fn active() -> Result<(), String> { + Ok(()) + } + let collection = PersonhoodCollection::LitePeople; + let member = proof::member_key([7; 32]); + let block = account_hex(&[1; 32]); + let response = |key: Vec, bytes: Vec| { + json!([{ + "block": block, + "changes": [[ + format!("0x{}", hex::encode(key)), + format!("0x{}", hex::encode(bytes)), + ]], + }]) + .to_string() + }; + let replies = [ + response( + ring::ring_keys_status_key(collection, 1), + (1u32, 0u32, None::).encode(), + ), + response(ring::ring_keys_key(collection, 1, 0), vec![member].encode()), + response( + ring::ring_keys_status_key(collection, 0), + (1u32, 1u32, None::).encode(), + ), + response(ring::ring_keys_key(collection, 0, 0), vec![member].encode()), + ]; + let chain = ReadOnlyChain { + rpc: RpcClient::new(HostRpcClient::new(ScriptedRpc::new( + replies.iter().map(String::as_str), + ))), + metadata: decode_metadata(include_bytes!( + "../../../tests/fixtures/paseo-next-v2-metadata-v16.scale" + )) + .unwrap(), + observation: AllowanceObservation { + genesis_hash: account_hex(&[0; 32]), + block_hash: block, + block_number: 19, + spec_version: 1, + chain_timestamp: 0, + }, + guard: &active, + }; + assert!( + futures::executor::block_on(chain.has_including_ring(collection, &member, 12, 1)) + .unwrap() + ); + } + + #[test] + fn metadata_decoding_rejects_trailing_bytes() { + let fixture = include_bytes!("../../../tests/fixtures/bulletin_paseo_metadata.scale"); + let mut corrupted = fixture.to_vec(); + corrupted.push(0); + assert!(decode_metadata(&corrupted).is_err()); + } +} diff --git a/rust/crates/truapi-server/src/runtime/statement_allowance/pgas.rs b/rust/crates/truapi-server/src/runtime/statement_allowance/pgas.rs index d140ab947..e12f001b3 100644 --- a/rust/crates/truapi-server/src/runtime/statement_allowance/pgas.rs +++ b/rust/crates/truapi-server/src/runtime/statement_allowance/pgas.rs @@ -15,13 +15,13 @@ use std::time::Duration; #[cfg(not(target_arch = "wasm32"))] use std::time::Instant; -#[cfg(target_arch = "wasm32")] -use web_time::Instant; use parity_scale_codec::{Decode, DecodeAll}; use scale_decode::DecodeAsType; use sp_crypto_hashing::twox_128; use thiserror::Error; +#[cfg(target_arch = "wasm32")] +use web_time::Instant; use super::collection::PersonhoodCollection; use super::extension::{AS_PGAS, Metadata, MetadataError}; @@ -164,7 +164,7 @@ pub struct PgasClaim<'a> { } /// `Assets.Account[(asset_id, who)]` storage key on Asset Hub. -fn pgas_balance_key(asset_id: u32, who: &[u8; 32]) -> Vec { +pub(super) fn pgas_balance_key(asset_id: u32, who: &[u8; 32]) -> Vec { [ twox_128(b"Assets").as_slice(), twox_128(b"Account").as_slice(), diff --git a/rust/crates/truapi-server/src/runtime/statement_allowance/ring.rs b/rust/crates/truapi-server/src/runtime/statement_allowance/ring.rs index 4edda67f8..8946131f9 100644 --- a/rust/crates/truapi-server/src/runtime/statement_allowance/ring.rs +++ b/rust/crates/truapi-server/src/runtime/statement_allowance/ring.rs @@ -67,13 +67,13 @@ const MEMBER_LEN: usize = 32; /// Fields read from `Members.Collections`. #[derive(Debug, PartialEq, Eq, DecodeAsType)] -struct CollectionInfo { - ring_size: RingExponent, +pub(super) struct CollectionInfo { + pub(super) ring_size: RingExponent, } /// Supported LitePeople ring domain sizes. #[derive(Debug, PartialEq, Eq, DecodeAsType)] -enum RingExponent { +pub(super) enum RingExponent { R2e9, R2e10, R2e14, @@ -81,7 +81,7 @@ enum RingExponent { impl RingExponent { /// Return the exponent represented by the runtime enum variant. - fn exponent(self) -> u8 { + pub(super) fn exponent(self) -> u8 { match self { Self::R2e9 => 9, Self::R2e10 => 10, @@ -117,7 +117,7 @@ pub struct RingParams { } /// `Members.CurrentRingIndex[id]` storage key. -fn current_ring_index_key(collection: PersonhoodCollection) -> Vec { +pub(super) fn current_ring_index_key(collection: PersonhoodCollection) -> Vec { [ twox_128(b"Members").as_slice(), twox_128(b"CurrentRingIndex").as_slice(), @@ -127,7 +127,7 @@ fn current_ring_index_key(collection: PersonhoodCollection) -> Vec { } /// `Members.Collections[id]` storage key. -fn collections_key(collection: PersonhoodCollection) -> Vec { +pub(super) fn collections_key(collection: PersonhoodCollection) -> Vec { [ twox_128(b"Members").as_slice(), twox_128(b"Collections").as_slice(), @@ -137,7 +137,7 @@ fn collections_key(collection: PersonhoodCollection) -> Vec { } /// `Members.RingKeysStatus[(id, ring_index)]` storage key. -fn ring_keys_status_key(collection: PersonhoodCollection, ring_index: u32) -> Vec { +pub(super) fn ring_keys_status_key(collection: PersonhoodCollection, ring_index: u32) -> Vec { [ twox_128(b"Members").as_slice(), twox_128(b"RingKeysStatus").as_slice(), @@ -159,7 +159,11 @@ fn ring_root_key(collection: PersonhoodCollection, ring_index: u32) -> Vec { } /// `Members.RingKeys[(id, ring_index, page)]` storage key. -fn ring_keys_key(collection: PersonhoodCollection, ring_index: u32, page: u32) -> Vec { +pub(super) fn ring_keys_key( + collection: PersonhoodCollection, + ring_index: u32, + page: u32, +) -> Vec { [ twox_128(b"Members").as_slice(), twox_128(b"RingKeys").as_slice(), diff --git a/rust/crates/truapi-server/src/runtime/statement_allowance/rpc.rs b/rust/crates/truapi-server/src/runtime/statement_allowance/rpc.rs index 26fb304b6..6b78de89c 100644 --- a/rust/crates/truapi-server/src/runtime/statement_allowance/rpc.rs +++ b/rust/crates/truapi-server/src/runtime/statement_allowance/rpc.rs @@ -16,10 +16,11 @@ use super::StatementAllowanceError; /// Timeout for an allowance registration extrinsic to reach a block. const SUBMIT_TIMEOUT: Duration = Duration::from_secs(120); -/// Error from the native JSON-RPC surface used by allowance allocation. +/// Error from the host-backed JSON-RPC surface used by allowance allocation. #[derive(Debug, Error)] pub enum RpcError { /// Opening a direct RPC URL failed. + #[cfg(not(target_arch = "wasm32"))] #[error("connect {url}: {source}")] Connect { /// RPC URL. @@ -49,6 +50,9 @@ pub enum RpcError { /// `chain_getFinalizedHead` did not return a hash string. #[error("chain_getFinalizedHead returned non-string")] FinalizedHeadNotString, + /// A read-only snapshot response was incomplete or malformed. + #[error("invalid snapshot RPC response: {0}")] + InvalidSnapshot(String), /// Extrinsic status subscription ended before inclusion. #[error("author_submitAndWatchExtrinsic subscription ended")] SubmitSubscriptionEnded, @@ -127,6 +131,28 @@ impl RpcClient { self.get_storage_maybe_at(key, Some(at)).await } + /// Read a complete storage batch at one block, rejecting ambiguous absence. + /// + /// Unlike the allocator's best-effort reader, every requested key must be + /// explicitly present (with a value or null) exactly once in the response. + pub async fn get_storage_many_at( + &self, + keys: &[Vec], + at: &str, + ) -> Result>>, StatementAllowanceError> { + if keys.is_empty() { + return Ok(Vec::new()); + } + let hex_keys: Vec = keys + .iter() + .map(|key| format!("0x{}", hex::encode(key))) + .collect(); + let response = self + .call("state_queryStorageAt", json!([hex_keys, at])) + .await?; + decode_storage_batch_at(&hex_keys, at, response) + } + async fn get_storage_maybe_at( &self, key: &[u8], @@ -260,19 +286,23 @@ enum ExtrinsicStatus { Pending, } +/// Classify one legacy `author_extrinsicUpdate` payload. +/// +/// `TransactionStatus` serializes its unit variants (`future`, `ready`, +/// `dropped`, `invalid`) as bare strings and the rest as single-key objects. fn extrinsic_status(status: &Value) -> ExtrinsicStatus { + if let Some(unit) = status.as_str() { + return match unit { + "dropped" | "invalid" => ExtrinsicStatus::Rejected(unit.to_string()), + _ => ExtrinsicStatus::Pending, + }; + } for key in ["finalized", "inBlock"] { if let Some(hash) = status.get(key).and_then(Value::as_str) { return ExtrinsicStatus::Included(hash.to_string()); } } - for key in [ - "invalid", - "dropped", - "usurped", - "retracted", - "finalityTimeout", - ] { + for key in ["usurped", "retracted", "finalityTimeout"] { if status.get(key).is_some() { return ExtrinsicStatus::Rejected(key.to_string()); } @@ -296,6 +326,52 @@ fn decode_hex(value: &str) -> Result, StatementAllowanceError> { .map_err(|err| RpcError::StorageHex(err).into()) } +fn decode_storage_batch_at( + keys: &[String], + at: &str, + response: Value, +) -> Result>>, StatementAllowanceError> { + let invalid = |reason: &str| RpcError::InvalidSnapshot(reason.to_string()); + let blocks = response + .as_array() + .ok_or_else(|| invalid("expected change sets"))?; + if blocks.len() != 1 || blocks[0].get("block").and_then(Value::as_str) != Some(at) { + return Err(invalid("change set does not name the requested block").into()); + } + let changes = blocks[0] + .get("changes") + .and_then(Value::as_array) + .ok_or_else(|| invalid("missing storage changes"))?; + let mut found = HashMap::with_capacity(keys.len()); + for change in changes { + let pair = change + .as_array() + .ok_or_else(|| invalid("invalid storage change"))?; + if pair.len() != 2 { + return Err(invalid("invalid storage change length").into()); + } + let key = pair[0] + .as_str() + .ok_or_else(|| invalid("invalid storage key"))?; + if !keys.iter().any(|expected| expected == key) || found.contains_key(key) { + return Err(invalid("unexpected or duplicate storage key").into()); + } + let value = match &pair[1] { + Value::Null => None, + Value::String(value) if value.starts_with("0x") => Some(decode_hex(value)?), + _ => return Err(invalid("storage value is neither hex nor null").into()), + }; + found.insert(key, value); + } + keys.iter() + .map(|key| { + found + .remove(key.as_str()) + .ok_or_else(|| invalid("incomplete storage batch").into()) + }) + .collect() +} + #[cfg(test)] pub(crate) mod testing { //! Scripted JSON-RPC transport for exercising request shapes in tests. @@ -417,7 +493,30 @@ mod tests { use serde_json::json; use super::testing::ScriptedRpc; - use super::{ExtrinsicStatus, HostRpcClient, RpcClient, extrinsic_status}; + use super::{ + ExtrinsicStatus, HostRpcClient, RpcClient, decode_storage_batch_at, extrinsic_status, + }; + + #[test] + fn snapshot_batches_distinguish_explicit_absence_from_incomplete_responses() { + let keys = vec!["0x01".to_string(), "0x02".to_string()]; + let complete = json!([{ "block": "0xat", "changes": [["0x02", "0x"], ["0x01", null]] }]); + assert_eq!( + decode_storage_batch_at(&keys, "0xat", complete).unwrap(), + vec![None, Some(Vec::new())], + ); + for response in [ + json!([]), + json!([{ "block": "0xat", "changes": [["0x01", null]] }]), + json!([{ "block": "0xother", "changes": [["0x01", null], ["0x02", null]] }]), + json!([{ "block": "0xat", "changes": [["0x01", null], ["0x01", null], ["0x02", null]] }]), + json!([{ "block": "0xat", "changes": [["0x01", false], ["0x02", null]] }]), + json!([{ "block": "0xat", "changes": [["0x01", "0xzz"], ["0x02", null]] }]), + json!([{ "block": "0xat", "changes": [["0x01", null], ["0x02", null], ["0x03", null]] }]), + ] { + assert!(decode_storage_batch_at(&keys, "0xat", response).is_err()); + } + } #[test] fn in_block_status_completes_submission() { @@ -436,14 +535,14 @@ mod tests { #[test] fn terminal_pool_statuses_reject_the_submission() { let statuses: Vec = [ - "invalid", - "dropped", - "usurped", - "retracted", - "finalityTimeout", + json!("invalid"), + json!("dropped"), + json!({"usurped": "0x1234"}), + json!({"retracted": "0x1234"}), + json!({"finalityTimeout": "0x1234"}), ] - .into_iter() - .map(|key| extrinsic_status(&json!({key: "0x1234"}))) + .iter() + .map(extrinsic_status) .collect(); assert_eq!( @@ -458,6 +557,21 @@ mod tests { ); } + #[test] + fn progress_statuses_keep_waiting() { + for status in [ + json!("future"), + json!("ready"), + json!({"broadcast": ["12D3KooW"]}), + ] { + assert_eq!( + extrinsic_status(&status), + ExtrinsicStatus::Pending, + "{status}" + ); + } + } + #[test] fn get_storage_at_pins_the_read_to_a_block() { let scripted = ScriptedRpc::new([r#""0x0102""#]); diff --git a/rust/crates/truapi-server/src/runtime/statement_allowance/slot.rs b/rust/crates/truapi-server/src/runtime/statement_allowance/slot.rs index 3e3300cee..8c9b93c96 100644 --- a/rust/crates/truapi-server/src/runtime/statement_allowance/slot.rs +++ b/rust/crates/truapi-server/src/runtime/statement_allowance/slot.rs @@ -132,11 +132,11 @@ pub enum SlotError { }, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] -struct StatementStoreAllowanceEntry { - account_id: [u8; 32], - seq: u32, - since: u64, +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, scale_decode::DecodeAsType)] +pub(super) struct StatementStoreAllowanceEntry { + pub(super) account_id: [u8; 32], + pub(super) seq: u32, + pub(super) since: u64, } /// A slot that is occupied, as observed by a scan. @@ -263,7 +263,7 @@ pub fn long_term_storage_alias( /// `Resources.StatementStoreAllowances[period][alias]` storage key. /// key1 = Identity(u32be period); key2 = Blake2_128Concat(alias). -fn statement_store_allowance_key(period: u32, alias: &[u8; 32]) -> Vec { +pub(super) fn statement_store_allowance_key(period: u32, alias: &[u8; 32]) -> Vec { [ twox_128(b"Resources").as_slice(), twox_128(b"StatementStoreAllowances").as_slice(), @@ -276,7 +276,7 @@ fn statement_store_allowance_key(period: u32, alias: &[u8; 32]) -> Vec { /// `Pgas.ClaimedGasAliases[day][alias]` storage key on Asset Hub. /// key1 = Identity(u32be day); key2 = Blake2_128Concat(alias). Presence alone /// marks the slot spent; the value is unit. -fn claimed_gas_alias_key(day: u32, alias: &[u8; 32]) -> Vec { +pub(super) fn claimed_gas_alias_key(day: u32, alias: &[u8; 32]) -> Vec { [ twox_128(b"Pgas").as_slice(), twox_128(b"ClaimedGasAliases").as_slice(), @@ -288,7 +288,7 @@ fn claimed_gas_alias_key(day: u32, alias: &[u8; 32]) -> Vec { /// `Resources.SpentLongTermStorageAliases[period][alias]` storage key. /// key1 = Identity(u32be period); key2 = Blake2_128Concat(alias). -fn spent_long_term_storage_alias_key(period: u32, alias: &[u8; 32]) -> Vec { +pub(super) fn spent_long_term_storage_alias_key(period: u32, alias: &[u8; 32]) -> Vec { [ twox_128(b"Resources").as_slice(), twox_128(b"SpentLongTermStorageAliases").as_slice(), @@ -369,7 +369,7 @@ pub fn replaceable_slot( } /// `Timestamp.Now` storage key. -fn timestamp_now_key() -> Vec { +pub(super) fn timestamp_now_key() -> Vec { [ twox_128(b"Timestamp").as_slice(), twox_128(b"Now").as_slice(), diff --git a/rust/crates/truapi-server/src/runtime/statement_allowance/view.rs b/rust/crates/truapi-server/src/runtime/statement_allowance/view.rs index 071dff062..f46d1b408 100644 --- a/rust/crates/truapi-server/src/runtime/statement_allowance/view.rs +++ b/rust/crates/truapi-server/src/runtime/statement_allowance/view.rs @@ -2,7 +2,7 @@ use parity_scale_codec::{Decode, DecodeAll, Encode}; use scale_info::{TypeDef, TypeDefPrimitive}; -use serde_json::{Value, json}; +use serde_json::Value; use thiserror::Error; use super::StatementAllowanceError; @@ -94,6 +94,7 @@ async fn read_u32( metadata: &Metadata, pallet: &'static str, function: &'static str, + at: Option<&str>, ) -> Result { let definition = metadata .view_function(pallet, function) @@ -106,11 +107,13 @@ async fn read_u32( })); } let id = definition.id; - if let Some(value) = metadata.cached_view_u32(&id) { - return Ok(value); + if at.is_none() { + if let Some(value) = metadata.cached_view_u32(&id) { + return Ok(value); + } } - let output = execute_no_args(rpc, pallet, function, id).await?; + let output = execute_no_args(rpc, pallet, function, id, at).await?; let primitive = metadata .registry() .resolve(definition.output_type) @@ -149,7 +152,9 @@ async fn read_u32( type_id: definition.output_type, })), }?; - metadata.cache_view_u32(id, value); + if at.is_none() { + metadata.cache_view_u32(id, value); + } Ok(value) } @@ -158,7 +163,17 @@ pub(super) async fn read_resource_u32( metadata: &Metadata, function: &'static str, ) -> Result { - read_u32(rpc, metadata, "Resources", function).await + read_u32(rpc, metadata, "Resources", function, None).await +} + +/// Resolve a runtime policy against the same block as an inspection's state. +pub(super) async fn read_resource_u32_at( + rpc: &RpcClient, + metadata: &Metadata, + function: &'static str, + at: &str, +) -> Result { + read_u32(rpc, metadata, "Resources", function, Some(at)).await } pub(super) fn supports_resource_u32(metadata: &Metadata, function: &'static str) -> bool { @@ -182,18 +197,18 @@ async fn execute_no_args( pallet: &'static str, function: &'static str, id: [u8; 32], + at: Option<&str>, ) -> Result, StatementAllowanceError> { let mut arguments = id.to_vec(); Vec::::new().encode_to(&mut arguments); - let response = rpc - .call( - "state_call", - json!([ - EXECUTE_VIEW_FUNCTION, - format!("0x{}", hex::encode(arguments)) - ]), - ) - .await?; + let mut params = vec![ + Value::String(EXECUTE_VIEW_FUNCTION.to_string()), + Value::String(format!("0x{}", hex::encode(arguments))), + ]; + if let Some(at) = at { + params.push(Value::String(at.to_string())); + } + let response = rpc.call("state_call", Value::Array(params)).await?; decode_response(pallet, function, response) } @@ -268,6 +283,7 @@ mod tests { "Resources", "get_lite_stmt_store_slots_per_period", id, + None, )) .unwrap(); @@ -316,6 +332,7 @@ mod tests { &metadata, "Resources", "get_stmt_store_slots_per_period", + None, )) .unwrap(); let second = futures::executor::block_on(read_u32( @@ -323,6 +340,7 @@ mod tests { &metadata, "Resources", "get_stmt_store_slots_per_period", + None, )) .unwrap(); @@ -352,6 +370,7 @@ mod tests { &metadata, "Resources", "get_stmt_store_replacement_cooldown", + None, )) .is_err() ); @@ -360,6 +379,7 @@ mod tests { &metadata, "Resources", "get_stmt_store_replacement_cooldown", + None, )) .unwrap(); diff --git a/rust/crates/truapi-server/src/runtime/statement_store_rpc.rs b/rust/crates/truapi-server/src/runtime/statement_store_rpc.rs index b5537230b..e010913a3 100644 --- a/rust/crates/truapi-server/src/runtime/statement_store_rpc.rs +++ b/rust/crates/truapi-server/src/runtime/statement_store_rpc.rs @@ -64,7 +64,7 @@ impl StatementStoreRpc { } /// Open a People-chain RPC client already scoped to its genesis hash, for - /// the native allowance paths that key the chain-context cache by it. + /// allowance paths that key the chain-context cache by it. pub(crate) async fn chain_client( &self, label: &'static str, diff --git a/rust/crates/truapi-server/src/runtime/tests.rs b/rust/crates/truapi-server/src/runtime/tests.rs index 0c17861b5..19dfbce1b 100644 --- a/rust/crates/truapi-server/src/runtime/tests.rs +++ b/rust/crates/truapi-server/src/runtime/tests.rs @@ -2198,6 +2198,51 @@ fn get_account_derives_rfc0022_product_key() { ); } +#[test] +fn get_account_answers_when_host_storage_never_returns() { + // A host whose storage never answers must not be able to park the request. + // Resolving an own-product account reads the persisted subtree first, and + // that read is a host callback: without a deadline the call never settles, + // so the product waits forever with no response and no error frame + // (paritytech/dotli-community#271). + let host = ProductRuntimeHost::new( + Arc::new(StubPlatform { + core_storage_pending: true, + ..Default::default() + }), + runtime_config("myapp.dot"), + test_spawner(), + ); + host.test_session_state().set_session(sso_session_info()); + let request = HostAccountGetRequest::V1(v01::HostAccountGetRequest { + product_account_id: account_id("myapp.dot", 0), + }); + // The caller's deadline is what bounds it; a short one keeps the test fast + // and proves the bound is the context's rather than a fixed sleep. + let mut cx = CallContext::default(); + cx.set_timeout(std::time::Duration::from_millis(200)); + + let outcome = futures::executor::block_on(async { + use futures::FutureExt; + use futures::pin_mut; + let call = host.get_account(&cx, request).fuse(); + let guard = futures_timer::Delay::new(std::time::Duration::from_secs(10)).fuse(); + pin_mut!(call, guard); + futures::select! { + result = call => Some(result), + () = guard => None, + } + }); + + let Some(result) = outcome else { + panic!("get_account never answered a host whose storage does not return"); + }; + assert!( + result.is_err(), + "a host that cannot answer its own storage must not resolve an account" + ); +} + #[test] fn get_account_own_product_prompts_and_rejects_on_a_cold_subtree() { let host = ProductRuntimeHost::new( diff --git a/rust/crates/truapi-server/src/wasm.rs b/rust/crates/truapi-server/src/wasm.rs index cf25d9760..9f580f92e 100644 --- a/rust/crates/truapi-server/src/wasm.rs +++ b/rust/crates/truapi-server/src/wasm.rs @@ -1216,14 +1216,19 @@ impl WasmSigningHostRuntime { let bridge = Arc::new(JsBridge::from_js(&callbacks)?); let WasmPlatformAdapters { platform, + chat_platform, + status_host, pocket_platform, - .. } = wasm_platform(bridge); let spawner: Spawner = Arc::new(|fut| { wasm_bindgen_futures::spawn_local(fut); }); let host_config = signing_host_config_from_js(&host_config)?; - let runtime = SigningHostRuntime::new(platform, host_config, spawner); + let runtime = + SigningHostRuntime::with_chat_platform(platform, host_config, spawner, chat_platform); + if let Some(status_host) = status_host { + runtime.set_permission_status_host(status_host); + } if let Some(pocket_platform) = pocket_platform { runtime.set_pocket_platform(pocket_platform); } @@ -1255,6 +1260,92 @@ impl WasmSigningHostRuntime { self.runtime.disconnect_session().await; } + /// Read the active local session's X25519 chat identity private key. + #[wasm_bindgen(js_name = sessionChatIdentityKey)] + pub fn session_chat_identity_key(&self) -> Option> { + self.runtime + .session_chat_identity_key() + .map(|key| key.to_vec()) + } + + /// Read this browser's X25519 encryption secret, generating and persisting + /// it on first read. + #[wasm_bindgen(js_name = deviceEncryptionKey)] + pub async fn device_encryption_key(&self) -> Result, JsValue> { + self.runtime + .device_encryption_key() + .await + .map(|key| key.to_vec()) + .map_err(generic_error_to_js) + } + + /// Resolve a product's hard-subtree public key from the active local + /// signing session. + #[wasm_bindgen(js_name = productSubtreePublicKey)] + pub async fn product_subtree_public_key( + &self, + product_id: String, + timeout_ms: Option, + ) -> Result>, JsValue> { + self.runtime + .product_subtree_public_key(&product_id, timeout_ms) + .await + .map(|key| key.map(|key| key.to_vec())) + .map_err(generic_error_to_js) + } + + /// Read one permission authorization status for a product. + #[wasm_bindgen(js_name = permissionAuthorizationStatus)] + pub async fn permission_authorization_status( + &self, + product_id: String, + payload: Vec, + ) -> Result { + let request = decode_permission_authorization_request(&payload)?; + let status = self + .runtime + .permission_authorization_status(&product_id, request) + .await + .map_err(generic_error_to_js)?; + Ok(permission_authorization_status_to_js(status)) + } + + /// Read permission authorization statuses for a product. + #[wasm_bindgen(js_name = permissionAuthorizationStatuses)] + pub async fn permission_authorization_statuses( + &self, + product_id: String, + payloads: Array, + ) -> Result { + let requests = decode_permission_authorization_requests(&payloads)?; + let statuses = self + .runtime + .permission_authorization_statuses(&product_id, requests) + .await + .map_err(generic_error_to_js)?; + let values = Array::new(); + for status in statuses { + values.push(&permission_authorization_status_to_js(status)); + } + Ok(values) + } + + /// Update one stored permission authorization status for a product. + #[wasm_bindgen(js_name = setPermissionAuthorizationStatus)] + pub async fn set_permission_authorization_status( + &self, + product_id: String, + payload: Vec, + status: String, + ) -> Result<(), JsValue> { + let request = decode_permission_authorization_request(&payload)?; + let status = permission_authorization_status_from_js(&status)?; + self.runtime + .set_permission_authorization_status(&product_id, request, status) + .await + .map_err(generic_error_to_js) + } + /// Activate a wallet-local session from raw BIP-39 entropy. #[wasm_bindgen(js_name = activateLocalSession)] pub async fn activate_local_session(&self, secret: Vec) -> Result<(), JsValue> { @@ -1277,6 +1368,78 @@ impl WasmSigningHostRuntime { .map_err(generic_error_to_js) } + /// Capture an opaque activation fence and its UID account. + #[wasm_bindgen(js_name = localIdentityContext)] + pub fn local_identity_context(&self) -> Result { + let context = self + .runtime + .local_identity_context() + .map_err(generic_error_to_js)?; + let json = serde_json::to_string(&context) + .map_err(|error| JsValue::from_str(&error.to_string()))?; + js_sys::JSON::parse(&json) + } + + /// Return the UID public key followed by its native sr25519 backend-auth proof. + #[wasm_bindgen(js_name = localIdentityAuthProof)] + pub fn local_identity_auth_proof( + &self, + activation_id: String, + challenge: Vec, + ) -> Result, JsValue> { + self.runtime + .local_identity_auth_proof(&activation_id, &challenge) + .map_err(generic_error_to_js) + } + + /// Build registration JSON without exporting entropy or implementing proofs in JavaScript. + #[wasm_bindgen(js_name = localLiteRegistrationBody)] + pub async fn local_lite_registration_body( + &self, + activation_id: String, + username_base: String, + verifier: Vec, + ) -> Result { + let verifier = verifier + .as_slice() + .try_into() + .map_err(|_| JsValue::from_str("attester must be 32 bytes"))?; + self.runtime + .local_lite_registration_body(&activation_id, &username_base, verifier) + .await + .map_err(generic_error_to_js) + } + + /// Install freshly verified dotNS metadata only for the captured local activation. + #[wasm_bindgen(js_name = refreshLocalIdentity)] + pub async fn refresh_local_identity(&self, activation_id: String) -> Result { + let identity = self + .runtime + .refresh_local_identity_for(&activation_id) + .await + .map_err(generic_error_to_js)?; + let json = serde_json::to_string(&identity) + .map_err(|error| JsValue::from_str(&error.to_string()))?; + js_sys::JSON::parse(&json) + } + + /// Read finalized allowance state for this activation; never allocate or sign. + #[wasm_bindgen(js_name = getWalletAllowanceSnapshot)] + pub async fn get_wallet_allowance_snapshot( + &self, + activation_id: String, + product_ids: Vec, + ) -> Result { + let snapshot = self + .runtime + .get_wallet_allowance_snapshot(&activation_id, product_ids) + .await + .map_err(generic_error_to_js)?; + let json = serde_json::to_string(&snapshot) + .map_err(|error| JsValue::from_str(&error.to_string()))?; + js_sys::JSON::parse(&json) + } + /// Revoke one product's grants from the current local activation. #[wasm_bindgen(js_name = clearProductState)] pub async fn clear_product_state(&self, product_id: String) -> Result<(), JsValue> { diff --git a/rust/crates/truapi/Cargo.toml b/rust/crates/truapi/Cargo.toml index 1bf8af133..a0d92a0a6 100644 --- a/rust/crates/truapi/Cargo.toml +++ b/rust/crates/truapi/Cargo.toml @@ -6,18 +6,24 @@ license.workspace = true description = "TrUAPI trait and type definitions" [dependencies] -async-trait = "0.1" -derive_more = { version = "2", features = ["display"] } -futures = "0.3" -hex = "0.4" -parity-scale-codec = { version = "3", features = ["derive"] } +async-trait = { version = "0.1", optional = true } +derive_more = { version = "2", default-features = false, features = ["display"] } +futures = { version = "0.3", optional = true } +hex = { version = "0.4", default-features = false, features = ["alloc"] } +parity-scale-codec = { version = "3", default-features = false, features = ["derive"] } truapi-macros = { path = "../truapi-macros" } uniffi = { workspace = true, optional = true } [features] -default = [] +default = ["runtime"] +# Async host traits, cancellation, and subscription primitives. +runtime = [ + "dep:async-trait", + "dep:futures", + "parity-scale-codec/std", +] # FFI derives on the protocol types consumed by native hosts. -uniffi = ["dep:uniffi"] +uniffi = ["runtime", "dep:uniffi"] [lints] workspace = true diff --git a/rust/crates/truapi/src/lib.rs b/rust/crates/truapi/src/lib.rs index e8af80e0d..3ea0eb28b 100644 --- a/rust/crates/truapi/src/lib.rs +++ b/rust/crates/truapi/src/lib.rs @@ -1,3 +1,4 @@ +#![cfg_attr(not(feature = "runtime"), no_std)] #![allow( clippy::double_must_use, reason = "async-trait generates must_use futures for async trait methods" @@ -11,21 +12,35 @@ //! still guarantee `Send` futures. Implementations must annotate their impl //! blocks with `#[truapi::async_trait]`. +extern crate alloc; + +use alloc::string::String; +#[cfg(feature = "runtime")] +use alloc::{boxed::Box, format, vec::Vec}; use core::convert::Infallible; +#[cfg(feature = "runtime")] use core::fmt; +#[cfg(feature = "runtime")] use core::future::Future; +#[cfg(feature = "runtime")] use core::mem; +#[cfg(feature = "runtime")] use core::pin::Pin; +#[cfg(feature = "runtime")] use core::task::{Context, Poll, Waker}; +#[cfg(feature = "runtime")] use core::time::Duration; -use std::sync::Arc; -use std::sync::Mutex; +#[cfg(feature = "runtime")] +use std::sync::{Arc, Mutex}; +#[cfg(feature = "runtime")] use futures::Stream; use parity_scale_codec::{Decode, Encode}; +#[cfg(feature = "runtime")] pub use async_trait::async_trait; +#[cfg(feature = "runtime")] pub mod api; pub mod v01; pub mod v02; @@ -277,17 +292,20 @@ pub type FrameworkOnlyError = CallError; /// when a runtime explicitly cancels it, or when an attached timeout elapses. /// Subscription runtimes can cancel this token when the peer sends `_stop` or /// disconnects. +#[cfg(feature = "runtime")] #[derive(Clone, Default)] pub struct CancellationToken { inner: Arc, } #[derive(Default)] +#[cfg(feature = "runtime")] struct CancellationInner { state: Mutex, } #[derive(Default)] +#[cfg(feature = "runtime")] struct CancellationState { reason: Option, next_id: u64, @@ -295,6 +313,7 @@ struct CancellationState { } /// Cause attached to a cancelled call. +#[cfg(feature = "runtime")] #[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)] pub enum CancellationReason { /// The caller or runtime explicitly cancelled the call. @@ -309,6 +328,7 @@ pub enum CancellationReason { } /// Render a timeout as whole seconds when possible, milliseconds otherwise. +#[cfg(feature = "runtime")] fn format_timeout(timeout: &Duration) -> String { if timeout.subsec_millis() == 0 { format!("{}s", timeout.as_secs()) @@ -318,11 +338,13 @@ fn format_timeout(timeout: &Duration) -> String { } /// Future resolved when a [`CancellationToken`] is cancelled. +#[cfg(feature = "runtime")] pub struct CancellationFuture { inner: Arc, id: Option, } +#[cfg(feature = "runtime")] impl fmt::Debug for CancellationToken { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("CancellationToken") @@ -331,6 +353,7 @@ impl fmt::Debug for CancellationToken { } } +#[cfg(feature = "runtime")] impl CancellationToken { /// Mark the token as cancelled. pub fn cancel(&self) { @@ -376,6 +399,7 @@ impl CancellationToken { } } +#[cfg(feature = "runtime")] impl Future for CancellationFuture { type Output = CancellationReason; @@ -407,6 +431,7 @@ impl Future for CancellationFuture { } } +#[cfg(feature = "runtime")] impl Drop for CancellationFuture { fn drop(&mut self) { let Some(id) = self.id.take() else { @@ -421,6 +446,7 @@ impl Drop for CancellationFuture { } /// Ambient context passed to every trait method. +#[cfg(feature = "runtime")] #[derive(Clone, Default)] pub struct CallContext { request_id: RequestId, @@ -428,6 +454,7 @@ pub struct CallContext { timeout: Option, } +#[cfg(feature = "runtime")] impl CallContext { /// Construct a context bound to the given `request_id` with a fresh cancellation token. pub fn with_request_id(request_id: RequestId) -> Self { @@ -475,10 +502,12 @@ impl CallContext { /// ends it: the runtime encodes that value as the `_interrupt` payload and /// polls no further. A stream that ends without an `Err` interrupts with /// `Ok(())`, which the peer reads as a normal completion. +#[cfg(feature = "runtime")] pub struct Subscription { inner: Pin> + Send>>, } +#[cfg(feature = "runtime")] impl Stream for Subscription { type Item = Result; @@ -487,6 +516,7 @@ impl Stream for Subscription { } } +#[cfg(feature = "runtime")] impl Subscription { /// Creates a subscription from a stream of items and at most one /// terminating interrupt. @@ -512,7 +542,7 @@ impl Subscription { } } -#[cfg(test)] +#[cfg(all(test, feature = "runtime"))] mod tests { use super::*; diff --git a/rust/crates/truapi/src/v01/account.rs b/rust/crates/truapi/src/v01/account.rs index b65acdd2d..171e76bd3 100644 --- a/rust/crates/truapi/src/v01/account.rs +++ b/rust/crates/truapi/src/v01/account.rs @@ -1,4 +1,5 @@ use crate::v01::transaction::GenesisHash; +use alloc::{string::String, vec::Vec}; use parity_scale_codec::{Decode, Encode}; /// Account selector within a product subtree. Encodes as diff --git a/rust/crates/truapi/src/v01/chain.rs b/rust/crates/truapi/src/v01/chain.rs index 589310cf2..09dff605f 100644 --- a/rust/crates/truapi/src/v01/chain.rs +++ b/rust/crates/truapi/src/v01/chain.rs @@ -1,3 +1,4 @@ +use alloc::{string::String, vec::Vec}; use parity_scale_codec::{Decode, Encode}; use super::common::GenericError; diff --git a/rust/crates/truapi/src/v01/chat.rs b/rust/crates/truapi/src/v01/chat.rs index 87539342d..b54cd99ba 100644 --- a/rust/crates/truapi/src/v01/chat.rs +++ b/rust/crates/truapi/src/v01/chat.rs @@ -1,3 +1,5 @@ +use alloc::{string::String, vec::Vec}; + use parity_scale_codec::{Decode, Encode}; /// Request to create a chat room. diff --git a/rust/crates/truapi/src/v01/coin_payment.rs b/rust/crates/truapi/src/v01/coin_payment.rs index 71580a6e6..12b84771a 100644 --- a/rust/crates/truapi/src/v01/coin_payment.rs +++ b/rust/crates/truapi/src/v01/coin_payment.rs @@ -1,3 +1,4 @@ +use alloc::{string::String, vec::Vec}; use parity_scale_codec::{Decode, Encode}; /// RFC 0017 CoinPayment purse identifier. diff --git a/rust/crates/truapi/src/v01/common.rs b/rust/crates/truapi/src/v01/common.rs index 49cbe1228..e06f31633 100644 --- a/rust/crates/truapi/src/v01/common.rs +++ b/rust/crates/truapi/src/v01/common.rs @@ -1,3 +1,4 @@ +use alloc::string::String; use parity_scale_codec::{Decode, Encode}; /// Generic error payload carrying a human-readable reason string. Used by many diff --git a/rust/crates/truapi/src/v01/entropy.rs b/rust/crates/truapi/src/v01/entropy.rs index 7a56e4524..1039a4a6d 100644 --- a/rust/crates/truapi/src/v01/entropy.rs +++ b/rust/crates/truapi/src/v01/entropy.rs @@ -1,3 +1,4 @@ +use alloc::{string::String, vec::Vec}; use parity_scale_codec::{Decode, Encode}; /// Error from [`crate::api::Entropy::derive`] (RFC 0007). diff --git a/rust/crates/truapi/src/v01/local_storage.rs b/rust/crates/truapi/src/v01/local_storage.rs index d3adf7997..58d935886 100644 --- a/rust/crates/truapi/src/v01/local_storage.rs +++ b/rust/crates/truapi/src/v01/local_storage.rs @@ -1,3 +1,4 @@ +use alloc::{string::String, vec::Vec}; use derive_more::Display; use parity_scale_codec::{Decode, Encode}; diff --git a/rust/crates/truapi/src/v01/locale.rs b/rust/crates/truapi/src/v01/locale.rs index bc244e19f..f3168d83c 100644 --- a/rust/crates/truapi/src/v01/locale.rs +++ b/rust/crates/truapi/src/v01/locale.rs @@ -1,3 +1,4 @@ +use alloc::string::String; use parity_scale_codec::{Decode, Encode}; /// Locale the host currently presents its interface in, pushed to subscribers. diff --git a/rust/crates/truapi/src/v01/notifications.rs b/rust/crates/truapi/src/v01/notifications.rs index d61633cc5..218d9201a 100644 --- a/rust/crates/truapi/src/v01/notifications.rs +++ b/rust/crates/truapi/src/v01/notifications.rs @@ -1,3 +1,4 @@ +use alloc::string::String; use parity_scale_codec::{Decode, Encode}; /// Opaque identifier for a push notification, unique per product. diff --git a/rust/crates/truapi/src/v01/payment.rs b/rust/crates/truapi/src/v01/payment.rs index 7b26901c3..ed3c14f3c 100644 --- a/rust/crates/truapi/src/v01/payment.rs +++ b/rust/crates/truapi/src/v01/payment.rs @@ -1,3 +1,4 @@ +use alloc::{string::String, vec::Vec}; use parity_scale_codec::{Decode, Encode}; use super::account::DerivationIndex; diff --git a/rust/crates/truapi/src/v01/permissions.rs b/rust/crates/truapi/src/v01/permissions.rs index b4d2c29e2..0fc09c966 100644 --- a/rust/crates/truapi/src/v01/permissions.rs +++ b/rust/crates/truapi/src/v01/permissions.rs @@ -1,3 +1,4 @@ +use alloc::{string::String, vec::Vec}; use derive_more::Display; use parity_scale_codec::{Decode, Encode}; diff --git a/rust/crates/truapi/src/v01/pocket.rs b/rust/crates/truapi/src/v01/pocket.rs index 80bea6b00..af12c003d 100644 --- a/rust/crates/truapi/src/v01/pocket.rs +++ b/rust/crates/truapi/src/v01/pocket.rs @@ -1,3 +1,5 @@ +use alloc::{string::String, vec::Vec}; + use parity_scale_codec::{Decode, Encode}; /// One of the calling product's Pocket cards. diff --git a/rust/crates/truapi/src/v01/preimage.rs b/rust/crates/truapi/src/v01/preimage.rs index 808066749..4ec9654dc 100644 --- a/rust/crates/truapi/src/v01/preimage.rs +++ b/rust/crates/truapi/src/v01/preimage.rs @@ -1,3 +1,4 @@ +use alloc::{string::String, vec::Vec}; use parity_scale_codec::{Decode, Encode}; /// Preimage submission error. diff --git a/rust/crates/truapi/src/v01/renderer.rs b/rust/crates/truapi/src/v01/renderer.rs index 1676e1f9f..1859137bf 100644 --- a/rust/crates/truapi/src/v01/renderer.rs +++ b/rust/crates/truapi/src/v01/renderer.rs @@ -1,5 +1,6 @@ //! Product-rendered body trees and the contexts that name them. +use alloc::{string::String, vec::Vec}; use parity_scale_codec::{Compact, Decode, Encode, OptionBool}; /// A size in logical pixels, SCALE-encoded as `Compact`. @@ -518,6 +519,7 @@ pub struct HostRendererActionSubscribeItem { #[cfg(test)] mod tests { use super::*; + use alloc::{string::ToString, vec}; #[derive(Encode)] struct RendererWireComponent

{ diff --git a/rust/crates/truapi/src/v01/resource_allocation.rs b/rust/crates/truapi/src/v01/resource_allocation.rs index 932ec1929..d9a1ef59a 100644 --- a/rust/crates/truapi/src/v01/resource_allocation.rs +++ b/rust/crates/truapi/src/v01/resource_allocation.rs @@ -1,3 +1,4 @@ +use alloc::{string::String, vec::Vec}; use parity_scale_codec::{Decode, Encode}; use super::account::DerivationIndex; diff --git a/rust/crates/truapi/src/v01/signing.rs b/rust/crates/truapi/src/v01/signing.rs index f32cc6139..d68da9f46 100644 --- a/rust/crates/truapi/src/v01/signing.rs +++ b/rust/crates/truapi/src/v01/signing.rs @@ -1,3 +1,4 @@ +use alloc::{string::String, vec::Vec}; use parity_scale_codec::{Decode, Encode, OptionBool}; use super::ProductAccountId; diff --git a/rust/crates/truapi/src/v01/statement_store.rs b/rust/crates/truapi/src/v01/statement_store.rs index f494a53af..a18c3a88f 100644 --- a/rust/crates/truapi/src/v01/statement_store.rs +++ b/rust/crates/truapi/src/v01/statement_store.rs @@ -1,3 +1,4 @@ +use alloc::{string::String, vec::Vec}; use parity_scale_codec::{Decode, Encode}; use super::ProductAccountId; diff --git a/rust/crates/truapi/src/v01/system.rs b/rust/crates/truapi/src/v01/system.rs index de67504fd..3ef3ec913 100644 --- a/rust/crates/truapi/src/v01/system.rs +++ b/rust/crates/truapi/src/v01/system.rs @@ -1,3 +1,4 @@ +use alloc::{string::String, vec::Vec}; use derive_more::Display; use parity_scale_codec::{Decode, Encode}; diff --git a/rust/crates/truapi/src/v01/theme.rs b/rust/crates/truapi/src/v01/theme.rs index bf7fd9edb..eb513ea32 100644 --- a/rust/crates/truapi/src/v01/theme.rs +++ b/rust/crates/truapi/src/v01/theme.rs @@ -1,3 +1,4 @@ +use alloc::string::String; use parity_scale_codec::{Decode, Encode}; /// Identifies a named theme. diff --git a/rust/crates/truapi/src/v01/transaction.rs b/rust/crates/truapi/src/v01/transaction.rs index b88e840f6..c2c50aa0a 100644 --- a/rust/crates/truapi/src/v01/transaction.rs +++ b/rust/crates/truapi/src/v01/transaction.rs @@ -1,3 +1,4 @@ +use alloc::{string::String, vec::Vec}; use parity_scale_codec::{Decode, Encode}; use super::ProductAccountId; diff --git a/rust/crates/truapi/src/v02/local_storage.rs b/rust/crates/truapi/src/v02/local_storage.rs index b608e0f30..65f808a28 100644 --- a/rust/crates/truapi/src/v02/local_storage.rs +++ b/rust/crates/truapi/src/v02/local_storage.rs @@ -1,3 +1,4 @@ +use alloc::string::String; use derive_more::Display; use parity_scale_codec::{Decode, Encode}; diff --git a/rust/crates/truapi/src/versioned/account.rs b/rust/crates/truapi/src/versioned/account.rs index cedf77910..0d839183f 100644 --- a/rust/crates/truapi/src/versioned/account.rs +++ b/rust/crates/truapi/src/versioned/account.rs @@ -1,4 +1,5 @@ //! Versioned wrappers for [`Account`](crate::api::Account) methods. +use alloc::vec::Vec; use crate::v01; diff --git a/rust/crates/truapi/src/versioned/chat.rs b/rust/crates/truapi/src/versioned/chat.rs index 7092b4604..af1e160ca 100644 --- a/rust/crates/truapi/src/versioned/chat.rs +++ b/rust/crates/truapi/src/versioned/chat.rs @@ -23,6 +23,7 @@ truapi_macros::versioned_type! { #[cfg(test)] mod tests { use super::*; + use alloc::string::String; use parity_scale_codec::{Decode, Encode}; // Fixture is `ChatRegisterBotV1_request` output from triangle-js-sdks diff --git a/rust/crates/truapi/src/versioned/local_storage.rs b/rust/crates/truapi/src/versioned/local_storage.rs index 3ee37aea3..1c3dd41c2 100644 --- a/rust/crates/truapi/src/versioned/local_storage.rs +++ b/rust/crates/truapi/src/versioned/local_storage.rs @@ -11,6 +11,8 @@ //! product's storage for a write, and there is nothing for those requests to //! address. +use alloc::string::ToString; + use crate::versioned::{FromLatest, IntoLatest}; use crate::{v01, v02}; diff --git a/rust/crates/truapi/src/versioned/preimage.rs b/rust/crates/truapi/src/versioned/preimage.rs index af6538f27..98c7ddd99 100644 --- a/rust/crates/truapi/src/versioned/preimage.rs +++ b/rust/crates/truapi/src/versioned/preimage.rs @@ -1,4 +1,5 @@ //! Versioned wrappers for [`Preimage`](crate::api::Preimage) methods. +use alloc::vec::Vec; use crate::v01; diff --git a/rust/crates/truapi/src/versioned/renderer.rs b/rust/crates/truapi/src/versioned/renderer.rs index ff6231ce2..63522227f 100644 --- a/rust/crates/truapi/src/versioned/renderer.rs +++ b/rust/crates/truapi/src/versioned/renderer.rs @@ -14,6 +14,7 @@ truapi_macros::versioned_type! { #[cfg(test)] mod tests { use super::*; + use alloc::{vec, vec::Vec}; use parity_scale_codec::{Decode, Encode}; #[test] diff --git a/scripts/codegen.sh b/scripts/codegen.sh index 578d4aeaf..ae9cfe526 100755 --- a/scripts/codegen.sh +++ b/scripts/codegen.sh @@ -43,6 +43,7 @@ cargo run -p truapi-codegen -- \ --playground-output js/packages/truapi/src/playground \ --client-examples-output playground/test/generated/examples \ --rust-output rust/crates/truapi-server/src/generated \ + --rust-client-output rust/crates/truapi-client/src/generated.rs \ --platform-input target/doc/truapi_platform.json \ --platform-ts-output js/packages/truapi-host/src/generated \ --platform-wasm-adapter-output js/packages/truapi-host/src/generated \ @@ -50,6 +51,7 @@ cargo run -p truapi-codegen -- \ --explorer-output js/packages/truapi/src/explorer rustfmt +"$NIGHTLY_TOOLCHAIN" --edition 2024 \ + rust/crates/truapi-client/src/generated.rs \ rust/crates/truapi-server/src/generated/dispatcher.rs \ rust/crates/truapi-server/src/generated/wire_table.rs \ rust/crates/truapi-server/src/wasm/generated_bridge.rs @@ -87,5 +89,6 @@ echo "Generated client at js/packages/truapi/src/generated/" echo "Generated playground metadata at js/packages/truapi/src/playground/codegen/" echo "Generated client examples at playground/test/generated/examples/" echo "Generated Rust dispatcher at rust/crates/truapi-server/src/generated/" +echo "Generated no-std Rust client catalog at rust/crates/truapi-client/src/generated.rs" echo "Generated host-callbacks WASM adapter at js/packages/truapi-host/src/generated/" echo "Generated Rust WASM bridge at rust/crates/truapi-server/src/wasm/generated_bridge.rs"