From 06ac57a6dc20eb5ade6c13352f57e9ea5d909e05 Mon Sep 17 00:00:00 2001 From: pgherveou Date: Sun, 17 May 2026 01:20:34 +0000 Subject: [PATCH 01/49] feat: port Rust core runtime from truapi_next prototype Lands the Rust runtime layer behind the TrUAPI protocol: codegen-emitted dispatcher, platform abstraction crate, server runtime with frame/transport/ dispatcher/subscription, runtime adapter from platform traits, host_logic helpers, WS bridge for native WebView hosts, UniFFI native bridge, and wasm-bindgen surface for Web Workers. New crates: - truapi-platform: Storage, Navigation, Notifications, Permissions (split device/remote callbacks), Features, ChainProvider, JsonRpcConnection, Accounts, Signing, StatementStore, Preimage; Platform super-trait. - truapi-server: TrUApiCore, Dispatcher, Frame (with [requestId][disc][payload] envelope), Subscription lifecycle, PlatformRuntimeHost

adapter, host_logic/{dotns,features,permissions,session}, ws_bridge (feature-gated), native UniFFI bridge, wasm32 wasm-bindgen surface. - uniffi-bindgen-cli: thin wrapper around uniffi::uniffi_bindgen_main() for regenerating Kotlin/Swift bindings. Codegen: - truapi-codegen --rust-output emits dispatcher.rs and wire_table.rs. - Methods are keyed by snake_case(trait)_method so StatementStore::submit and Preimage::submit no longer collide. - Responses wrap in SCALE Result discriminant byte ([0x00, ok] / [0x01, err]) matching the @parity/truapi TS codec. - CallContext is constructed with the actual message request_id. truapi crate (additive only): - Display impls on v01 and versioned permission enums for modal copy and storage key construction in host_logic. Tests: 139 across the workspace (with ws-bridge feature), including WS round-trip against a real tokio-tungstenite client, frame snapshot, dotns scheme allowlist (rejects javascript:/file:/data:/vbscript:), permission cache isolation, session broadcast, dispatcher error path with Result discriminant. Relates to #96. Remaining phases tracked in #97-#103. --- Cargo.lock | 1595 ++++++++++++++++- rust/crates/truapi-codegen/Cargo.toml | 3 + rust/crates/truapi-codegen/src/main.rs | 15 + rust/crates/truapi-codegen/src/rust.rs | 349 ++++ .../truapi-codegen/src/rust/dispatcher.rs | 439 +++++ .../truapi-codegen/src/rust/wire_table.rs | 287 +++ .../tests/fixtures/sample.rustdoc.json | 3 + .../truapi-codegen/tests/golden/dispatcher.rs | 1131 ++++++++++++ .../truapi-codegen/tests/golden/wire_table.rs | 590 ++++++ .../truapi-codegen/tests/golden_rust_emit.rs | 179 ++ rust/crates/truapi-platform/Cargo.toml | 12 + rust/crates/truapi-platform/README.md | 28 + rust/crates/truapi-platform/src/lib.rs | 276 +++ .../truapi-platform/tests/object_safety.rs | 5 + rust/crates/truapi-server/Cargo.toml | 53 + rust/crates/truapi-server/README.md | 36 + rust/crates/truapi-server/src/core.rs | 368 ++++ rust/crates/truapi-server/src/debug_log.rs | 47 + rust/crates/truapi-server/src/dispatcher.rs | 302 ++++ rust/crates/truapi-server/src/frame.rs | 521 ++++++ .../truapi-server/src/generated/dispatcher.rs | 1412 +++++++++++++++ .../crates/truapi-server/src/generated/mod.rs | 4 + .../truapi-server/src/generated/wire_table.rs | 590 ++++++ .../truapi-server/src/host_logic/dotns.rs | 546 ++++++ .../truapi-server/src/host_logic/features.rs | 76 + .../truapi-server/src/host_logic/mod.rs | 10 + .../src/host_logic/permissions.rs | 387 ++++ .../truapi-server/src/host_logic/session.rs | 233 +++ rust/crates/truapi-server/src/lib.rs | 52 + rust/crates/truapi-server/src/native.rs | 706 ++++++++ rust/crates/truapi-server/src/runtime.rs | 889 +++++++++ rust/crates/truapi-server/src/subscription.rs | 297 +++ rust/crates/truapi-server/src/transport.rs | 12 + rust/crates/truapi-server/src/wasm.rs | 799 +++++++++ rust/crates/truapi-server/src/ws_bridge.rs | 629 +++++++ .../truapi-server/tests/golden_frame.rs | 52 + .../tests/snapshots/golden-account-get.bin | Bin 0 -> 14 bytes .../truapi-server/tests/wire_result_shape.rs | 295 +++ .../tests/wire_table_ts_parity.rs | 175 ++ rust/crates/truapi/src/v01/permissions.rs | 203 +++ .../truapi/src/versioned/permissions.rs | 43 + rust/crates/uniffi-bindgen-cli/Cargo.toml | 13 + rust/crates/uniffi-bindgen-cli/README.md | 23 + rust/crates/uniffi-bindgen-cli/src/main.rs | 9 + scripts/codegen.sh | 4 + 45 files changed, 13624 insertions(+), 74 deletions(-) create mode 100644 rust/crates/truapi-codegen/src/rust.rs create mode 100644 rust/crates/truapi-codegen/src/rust/dispatcher.rs create mode 100644 rust/crates/truapi-codegen/src/rust/wire_table.rs create mode 100644 rust/crates/truapi-codegen/tests/fixtures/sample.rustdoc.json create mode 100644 rust/crates/truapi-codegen/tests/golden/dispatcher.rs create mode 100644 rust/crates/truapi-codegen/tests/golden/wire_table.rs create mode 100644 rust/crates/truapi-codegen/tests/golden_rust_emit.rs create mode 100644 rust/crates/truapi-platform/Cargo.toml create mode 100644 rust/crates/truapi-platform/README.md create mode 100644 rust/crates/truapi-platform/src/lib.rs create mode 100644 rust/crates/truapi-platform/tests/object_safety.rs create mode 100644 rust/crates/truapi-server/Cargo.toml create mode 100644 rust/crates/truapi-server/README.md create mode 100644 rust/crates/truapi-server/src/core.rs create mode 100644 rust/crates/truapi-server/src/debug_log.rs create mode 100644 rust/crates/truapi-server/src/dispatcher.rs create mode 100644 rust/crates/truapi-server/src/frame.rs create mode 100644 rust/crates/truapi-server/src/generated/dispatcher.rs create mode 100644 rust/crates/truapi-server/src/generated/mod.rs create mode 100644 rust/crates/truapi-server/src/generated/wire_table.rs create mode 100644 rust/crates/truapi-server/src/host_logic/dotns.rs create mode 100644 rust/crates/truapi-server/src/host_logic/features.rs create mode 100644 rust/crates/truapi-server/src/host_logic/mod.rs create mode 100644 rust/crates/truapi-server/src/host_logic/permissions.rs create mode 100644 rust/crates/truapi-server/src/host_logic/session.rs create mode 100644 rust/crates/truapi-server/src/lib.rs create mode 100644 rust/crates/truapi-server/src/native.rs create mode 100644 rust/crates/truapi-server/src/runtime.rs create mode 100644 rust/crates/truapi-server/src/subscription.rs create mode 100644 rust/crates/truapi-server/src/transport.rs create mode 100644 rust/crates/truapi-server/src/wasm.rs create mode 100644 rust/crates/truapi-server/src/ws_bridge.rs create mode 100644 rust/crates/truapi-server/tests/golden_frame.rs create mode 100644 rust/crates/truapi-server/tests/snapshots/golden-account-get.bin create mode 100644 rust/crates/truapi-server/tests/wire_result_shape.rs create mode 100644 rust/crates/truapi-server/tests/wire_table_ts_parity.rs create mode 100644 rust/crates/uniffi-bindgen-cli/Cargo.toml create mode 100644 rust/crates/uniffi-bindgen-cli/README.md create mode 100644 rust/crates/uniffi-bindgen-cli/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 26ca7750..2ba3b9a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,6 +64,80 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "askama" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7" +dependencies = [ + "askama_derive", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2", + "quote", + "rustc-hash", + "serde", + "serde_derive", + "syn", +] + +[[package]] +name = "askama_parser" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f" +dependencies = [ + "memchr", + "serde", + "serde_derive", + "winnow 0.7.15", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + [[package]] name = "bitvec" version = "1.0.1" @@ -76,12 +150,77 @@ dependencies = [ "wyz", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + [[package]] name = "byte-slice-cast" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + [[package]] name = "clap" version = "4.6.1" @@ -158,12 +297,98 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + [[package]] name = "funty" version = "2.0.0" @@ -241,6 +466,16 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +dependencies = [ + "gloo-timers", + "send_wrapper 0.4.0", +] + [[package]] name = "futures-util" version = "0.3.32" @@ -258,6 +493,80 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.17.0" @@ -276,6 +585,131 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "impl-trait-for-tuples" version = "0.2.3" @@ -294,7 +728,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.0", + "serde", + "serde_core", ] [[package]] @@ -318,6 +754,18 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "js-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + [[package]] name = "konst" version = "0.2.20" @@ -334,31 +782,94 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" [[package]] -name = "memchr" -version = "2.8.0" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] -name = "once_cell_polyfill" -version = "1.70.2" +name = "libc" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] -name = "parity-scale-codec" -version = "3.7.5" +name = "linux-raw-sys" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" -dependencies = [ - "arrayvec", - "bitvec", - "byte-slice-cast", - "const_format", - "impl-trait-for-tuples", - "parity-scale-codec-derive", - "rustversion", - "serde", +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", ] [[package]] @@ -373,12 +884,72 @@ dependencies = [ "syn", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -406,18 +977,118 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "send_wrapper" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" +dependencies = [ + "futures-core", +] + [[package]] name = "serde" version = "1.0.228" @@ -461,12 +1132,63 @@ dependencies = [ "zmij", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -484,6 +1206,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tap" version = "1.0.1" @@ -491,61 +1224,61 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" +name = "tempfile" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "serde_core", + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys", ] [[package]] -name = "toml_edit" -version = "0.25.11+spec-1.1.0" +name = "textwrap" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", + "smawk", ] [[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "winnow", + "thiserror-impl 1.0.69", ] [[package]] -name = "truapi" -version = "0.1.0" +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "futures", - "hex", - "parity-scale-codec", - "truapi-macros", + "thiserror-impl 2.0.18", ] [[package]] -name = "truapi-codegen" -version = "0.1.0" +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "anyhow", - "clap", - "convert_case", - "indoc", - "serde", - "serde_json", - "truapi", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "truapi-macros" -version = "0.1.0" +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -553,53 +1286,670 @@ dependencies = [ ] [[package]] -name = "unicode-ident" -version = "1.0.24" +name = "tinystr" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] [[package]] -name = "unicode-segmentation" -version = "1.13.2" +name = "tinyvec" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] [[package]] -name = "unicode-xid" -version = "0.2.6" +name = "tinyvec_macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] -name = "utf8parse" -version = "0.2.2" +name = "tokio" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys", +] [[package]] -name = "windows-link" -version = "0.2.1" +name = "tokio-macros" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "windows-sys" -version = "0.61.2" +name = "tokio-tungstenite" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" dependencies = [ - "windows-link", + "futures-util", + "log", + "tokio", + "tungstenite", ] [[package]] -name = "winnow" -version = "1.0.2" +name = "toml" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ - "memchr", + "serde", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow 1.0.2", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + +[[package]] +name = "truapi" +version = "0.1.0" +dependencies = [ + "futures", + "hex", + "parity-scale-codec", + "truapi-macros", +] + +[[package]] +name = "truapi-codegen" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "convert_case", + "indoc", + "serde", + "serde_json", + "tempfile", + "truapi", +] + +[[package]] +name = "truapi-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "truapi-platform" +version = "0.1.0" +dependencies = [ + "async-trait", + "futures", + "parity-scale-codec", + "truapi", +] + +[[package]] +name = "truapi-server" +version = "0.1.0" +dependencies = [ + "async-trait", + "futures", + "futures-timer", + "futures-util", + "getrandom 0.2.17", + "hex", + "js-sys", + "parity-scale-codec", + "pin-project", + "rand", + "send_wrapper 0.6.0", + "thiserror 1.0.69", + "tokio", + "tokio-tungstenite", + "truapi", + "truapi-platform", + "unicode-normalization", + "uniffi", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", +] + +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror 1.0.69", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "uniffi" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3291800a6b06569f7d3e15bdb6dc235e0f0c8bd3eb07177f430057feb076415f" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "clap", + "uniffi_bindgen", + "uniffi_core", + "uniffi_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi-bindgen-cli" +version = "0.1.0" +dependencies = [ + "uniffi", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a04b99fa7796eaaa7b87976a0dbdd1178dc1ee702ea00aca2642003aef9b669e" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck", + "indexmap", + "once_cell", + "serde", + "tempfile", + "textwrap", + "toml", + "uniffi_internal_macros", + "uniffi_meta", + "uniffi_pipeline", + "uniffi_udl", +] + +[[package]] +name = "uniffi_core" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38a9a27529ccff732f8efddb831b65b1e07f7dea3fd4cacd4a35a8c4b253b98" +dependencies = [ + "anyhow", + "bytes", + "once_cell", + "static_assertions", +] + +[[package]] +name = "uniffi_internal_macros" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09acd2ce09c777dd65ee97c251d33c8a972afc04873f1e3b21eb3492ade16933" +dependencies = [ + "anyhow", + "indexmap", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "uniffi_macros" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5596f178c4f7aafa1a501c4e0b96236a96bc2ef92bdb453d83e609dad0040152" +dependencies = [ + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn", + "toml", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beadc1f460eb2e209263c49c4f5b19e9a02e00a3b2b393f78ad10d766346ecff" +dependencies = [ + "anyhow", + "siphasher", + "uniffi_internal_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_pipeline" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd76b3ac8a2d964ca9fce7df21c755afb4c77b054a85ad7a029ad179cc5abb8a" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "tempfile", + "uniffi_internal_macros", +] + +[[package]] +name = "uniffi_udl" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319cf905911d70d5b97ce0f46f101619a22e9a189c8c46d797a9955e9233716" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "weedle2", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weedle2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + [[package]] name = "wyz" version = "0.5.1" @@ -609,6 +1959,103 @@ dependencies = [ "tap", ] +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/rust/crates/truapi-codegen/Cargo.toml b/rust/crates/truapi-codegen/Cargo.toml index 14fdc1f5..ed77e6d5 100644 --- a/rust/crates/truapi-codegen/Cargo.toml +++ b/rust/crates/truapi-codegen/Cargo.toml @@ -16,3 +16,6 @@ anyhow = "1" clap = { version = "4", features = ["derive"] } indoc = "2" convert_case = "0.6" + +[dev-dependencies] +tempfile = "3" diff --git a/rust/crates/truapi-codegen/src/main.rs b/rust/crates/truapi-codegen/src/main.rs index 4a2d5271..d2c9c657 100644 --- a/rust/crates/truapi-codegen/src/main.rs +++ b/rust/crates/truapi-codegen/src/main.rs @@ -1,7 +1,9 @@ use anyhow::{Context, Result}; use clap::Parser; +use std::path::PathBuf; use std::str::FromStr; +mod rust; mod rustdoc; mod ts; @@ -49,6 +51,14 @@ struct Cli { /// Output directory for the generated `@parity/truapi-host` TypeScript surface (optional). #[arg(long)] host_output: Option, + + /// Output directory for the generated Rust dispatcher / wire-table (optional). + /// + /// When set, emits `dispatcher.rs` and `wire_table.rs` for the + /// `truapi-server` crate to include. Phase 4 will wire this up to + /// `scripts/codegen.sh`; until then the flag is opt-in. + #[arg(long)] + rust_output: Option, } #[derive(Clone, Copy, Debug)] @@ -111,5 +121,10 @@ fn main() -> Result<()> { ts::generate_host(&api, path).with_context(|| format!("writing host package to {path}"))?; println!("Generated host package in {path}"); } + if let Some(path) = &cli.rust_output { + rust::generate(&api, path) + .with_context(|| format!("writing Rust dispatcher to {}", path.display()))?; + println!("Wrote Rust dispatcher to {}", path.display()); + } Ok(()) } diff --git a/rust/crates/truapi-codegen/src/rust.rs b/rust/crates/truapi-codegen/src/rust.rs new file mode 100644 index 00000000..064054f4 --- /dev/null +++ b/rust/crates/truapi-codegen/src/rust.rs @@ -0,0 +1,349 @@ +//! Rust code generation from extracted API definitions. +//! +//! Emits the server-side wire dispatcher (`dispatcher.rs`) and the +//! discriminant lookup table (`wire_table.rs`). The generated files are +//! intended to be included in the `truapi-server` crate (Phase 4); the +//! codegen itself only diffs string output. + +use std::fs; +use std::path::Path; + +use anyhow::Result; + +use crate::rustdoc::*; + +mod dispatcher; +mod wire_table; + +pub use dispatcher::generate_dispatcher; +pub use wire_table::generate_wire_table; + +/// Generates the Rust wire dispatcher and wire-table sources into `output_dir`. +pub fn generate(api: &ApiDefinition, output_dir: &Path) -> Result<()> { + fs::create_dir_all(output_dir)?; + let dispatcher = generate_dispatcher(api)?; + fs::write(output_dir.join("dispatcher.rs"), dispatcher)?; + let wire_table = generate_wire_table(api)?; + fs::write(output_dir.join("wire_table.rs"), wire_table)?; + Ok(()) +} + +/// Trait -> versioned-module mapping. Trait names are PascalCase +/// (`JsonRpc`, `LocalStorage`); module names are snake_case +/// (`jsonrpc`, `local_storage`). The mapping is irregular enough +/// (e.g. `JsonRpc` -> `jsonrpc`) that it is hardcoded. +const TRAIT_MODULE_MAP: &[(&str, &str)] = &[ + ("Account", "account"), + ("Chain", "chain"), + ("Chat", "chat"), + ("Entropy", "entropy"), + ("JsonRpc", "jsonrpc"), + ("LocalStorage", "local_storage"), + ("Payment", "payment"), + ("Permissions", "permissions"), + ("Preimage", "preimage"), + ("ResourceAllocation", "resource_allocation"), + ("Signing", "signing"), + ("StatementStore", "statement_store"), + ("System", "system"), + ("Theme", "theme"), +]; + +/// Returns the versioned-module name for a trait, falling back to a +/// snake_case conversion of the trait name when no explicit mapping is +/// declared. New traits should be added to [`TRAIT_MODULE_MAP`] so the +/// emission stays deterministic. +fn module_for_trait(trait_name: &str) -> String { + for (name, module) in TRAIT_MODULE_MAP { + if *name == trait_name { + return (*module).to_string(); + } + } + snake_case(trait_name) +} + +/// Returns the wire-protocol method name for a trait/method pair, used both +/// as the dispatcher's registration key and as the prefix of the action tag +/// (`{wire_method}_{request|response|...}`). The form is +/// `{trait_snake}_{method}` so collisions between sibling traits (e.g. +/// `StatementStore::submit` and `Preimage::submit`) become distinct keys +/// (`statement_store_submit`, `preimage_submit`). +pub(crate) fn wire_method_name(trait_name: &str, method_name: &str) -> String { + format!("{}_{}", snake_case(trait_name), method_name) +} + +/// Convert a PascalCase identifier into snake_case. +fn snake_case(name: &str) -> String { + let mut out = String::with_capacity(name.len() + 4); + for (idx, ch) in name.chars().enumerate() { + if ch.is_ascii_uppercase() { + if idx != 0 { + out.push('_'); + } + out.push(ch.to_ascii_lowercase()); + } else { + out.push(ch); + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_request_method(name: &str, request_id: u8) -> MethodDef { + MethodDef { + name: name.to_string(), + kind: MethodKind::Request, + params: vec![ParamDef { + name: "request".to_string(), + type_ref: TypeRef::Named { + name: "ReqWrapper".to_string(), + args: vec![], + }, + }], + return_type: ReturnType::Result { + ok: TypeRef::Named { + name: "RespWrapper".to_string(), + args: vec![], + }, + err: TypeRef::Named { + name: "CallError".to_string(), + args: vec![TypeRef::Named { + name: "ErrWrapper".to_string(), + args: vec![], + }], + }, + }, + wire: WireAttrs { + request_id: Some(request_id), + response_id: None, + start_id: None, + stop_id: None, + interrupt_id: None, + receive_id: None, + }, + docs: None, + } + } + + fn make_subscription_method(name: &str, start_id: u8) -> MethodDef { + MethodDef { + name: name.to_string(), + kind: MethodKind::Subscription, + params: vec![], + return_type: ReturnType::Subscription(TypeRef::Named { + name: "ItemWrapper".to_string(), + args: vec![], + }), + wire: WireAttrs { + request_id: None, + response_id: None, + start_id: Some(start_id), + stop_id: None, + interrupt_id: None, + receive_id: None, + }, + docs: None, + } + } + + fn parse_entries(src: &str) -> Vec<(u8, String)> { + // Lines look like ` WireEntry { method: "x", ... request_id: 7, ... },` + // For the assertion we extract (id, tag) pairs from the embedded + // helper comment block instead. Keep a simpler parser of + // `// id=NN tag="..."` lines emitted by the generator. + let mut out = Vec::new(); + for line in src.lines() { + let trimmed = line.trim(); + if let Some(rest) = trimmed.strip_prefix("// entry id=") { + let mut parts = rest.splitn(2, ' '); + let id = parts.next().unwrap().parse::().unwrap(); + let tag = parts + .next() + .unwrap() + .trim_start_matches("tag=\"") + .trim_end_matches('"') + .to_string(); + out.push((id, tag)); + } + } + out + } + + /// A single subscription method must reserve four consecutive wire + /// ids (start/stop/interrupt/receive) even when no sibling methods + /// exist to mask off-by-one errors. + #[test] + fn wire_table_subscribe_method_reserves_four_ids() { + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Account".to_string(), + methods: vec![make_subscription_method("connection_status_subscribe", 18)], + docs: None, + }], + public_trait_order: vec!["Account".to_string()], + types: vec![], + }; + + let src = generate_wire_table(&api).expect("generate_wire_table"); + let entries = parse_entries(&src); + assert_eq!( + entries, + vec![ + (18, "account_connection_status_subscribe_start".into()), + (19, "account_connection_status_subscribe_stop".into()), + (20, "account_connection_status_subscribe_interrupt".into()), + (21, "account_connection_status_subscribe_receive".into()), + ], + ); + } + + /// Two traits each declaring a method named `submit` must produce two + /// distinct, non-colliding wire method keys; the emitter prefixes by + /// the snake_case trait name (e.g. `statement_store_submit` / + /// `preimage_submit`). + #[test] + fn collision_safe_when_two_traits_share_method_name() { + let api = ApiDefinition { + traits: vec![ + TraitDef { + name: "StatementStore".to_string(), + methods: vec![make_request_method("submit", 62)], + docs: None, + }, + TraitDef { + name: "Preimage".to_string(), + methods: vec![make_request_method("submit", 68)], + docs: None, + }, + ], + public_trait_order: vec!["StatementStore".to_string(), "Preimage".to_string()], + types: vec![], + }; + + let dispatcher = generate_dispatcher(&api).expect("dispatcher"); + assert!( + dispatcher.contains("\"statement_store_submit\""), + "dispatcher missing prefixed StatementStore key:\n{dispatcher}" + ); + assert!( + dispatcher.contains("\"preimage_submit\""), + "dispatcher missing prefixed Preimage key:\n{dispatcher}" + ); + + let table = generate_wire_table(&api).expect("wire_table"); + let entries = parse_entries(&table); + assert!( + entries + .iter() + .any(|(_, tag)| tag == "statement_store_submit_request"), + "wire_table missing prefixed StatementStore tag:\n{table}" + ); + assert!( + entries + .iter() + .any(|(_, tag)| tag == "preimage_submit_request"), + "wire_table missing prefixed Preimage tag:\n{table}" + ); + } + + /// If a future change ever produces the same wire method key from two + /// different (trait, method) pairs, both emitters must fail loudly + /// rather than silently overwrite a handler. + #[test] + fn wire_table_rejects_method_name_collision() { + // `Foo::bar_baz` and `FooBar::baz` both snake-case to + // `foo_bar_baz`. The emitter must reject the pair. + let api = ApiDefinition { + traits: vec![ + TraitDef { + name: "Foo".to_string(), + methods: vec![make_request_method("bar_baz", 10)], + docs: None, + }, + TraitDef { + name: "FooBar".to_string(), + methods: vec![make_request_method("baz", 12)], + docs: None, + }, + ], + public_trait_order: vec!["Foo".to_string(), "FooBar".to_string()], + types: vec![], + }; + let err = generate_wire_table(&api).expect_err("duplicate wire method name must error"); + let msg = format!("{err}"); + assert!( + msg.contains("wire method name `foo_bar_baz` reused"), + "unexpected error message: {msg}", + ); + + let err = generate_dispatcher(&api).expect_err("duplicate wire method name must error"); + let msg = format!("{err}"); + assert!( + msg.contains("Wire method name `foo_bar_baz` registered twice"), + "unexpected dispatcher error message: {msg}", + ); + } + + /// Emission must be deterministic: running the codegen twice on the + /// same API produces byte-identical output. + #[test] + fn idempotent_emission() { + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Permissions".to_string(), + methods: vec![make_request_method("request_device_permission", 8)], + docs: None, + }], + public_trait_order: vec!["Permissions".to_string()], + types: vec![], + }; + + let dispatcher_a = generate_dispatcher(&api).expect("dispatcher a"); + let dispatcher_b = generate_dispatcher(&api).expect("dispatcher b"); + assert_eq!(dispatcher_a, dispatcher_b); + + let table_a = generate_wire_table(&api).expect("wire_table a"); + let table_b = generate_wire_table(&api).expect("wire_table b"); + assert_eq!(table_a, table_b); + } + + /// Methods with a `#[wire(request_id = N)]` annotation get a 2-id + /// slot (request/response). Methods with `#[wire(start_id = N)]` + /// get a 4-id slot (start/stop/interrupt/receive). The emitter + /// must enforce that, and reject collisions. + #[test] + fn wire_table_rejects_collisions() { + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Permissions".to_string(), + methods: vec![ + make_request_method("alpha", 10), + make_request_method("beta", 10), + ], + docs: None, + }], + public_trait_order: vec!["Permissions".to_string()], + types: vec![], + }; + let err = generate_wire_table(&api).expect_err("duplicate ids must error"); + let msg = format!("{err}"); + assert!( + msg.contains("wire id 10 reused"), + "unexpected error message: {msg}", + ); + } + + #[test] + fn module_for_trait_maps_irregular_names() { + assert_eq!(module_for_trait("JsonRpc"), "jsonrpc"); + assert_eq!(module_for_trait("LocalStorage"), "local_storage"); + assert_eq!( + module_for_trait("ResourceAllocation"), + "resource_allocation" + ); + assert_eq!(module_for_trait("Account"), "account"); + } +} diff --git a/rust/crates/truapi-codegen/src/rust/dispatcher.rs b/rust/crates/truapi-codegen/src/rust/dispatcher.rs new file mode 100644 index 00000000..35b82ab3 --- /dev/null +++ b/rust/crates/truapi-codegen/src/rust/dispatcher.rs @@ -0,0 +1,439 @@ +//! Emits `dispatcher.rs`: the server-side wire dispatcher that routes +//! incoming frames to the host trait implementation. +//! +//! For each method the emitter produces an `on_request` (or +//! `on_subscription`) registration that: +//! 1. SCALE-decodes the versioned request wrapper from the wire bytes. +//! 2. Calls the host trait method (which receives the wrapper directly +//! and matches `_::V1(inner)` internally). +//! 3. SCALE-encodes the versioned response wrapper back onto the wire. +//! +//! The generated file expects to live inside a `truapi-server` crate +//! and references `crate::dispatcher::Dispatcher`. The Phase 1 codegen +//! does not attempt to compile the output; only string-diff golden +//! tests guard it. + +use std::collections::BTreeMap; +use std::collections::BTreeSet; +use std::fmt::Write; + +use anyhow::{Result, bail}; + +use crate::rustdoc::*; + +use super::{module_for_trait, wire_method_name}; + +/// Emit the contents of `dispatcher.rs`. +pub fn generate_dispatcher(api: &ApiDefinition) -> Result { + let traits = order_traits(api)?; + + // Reject any duplicate wire method name across traits before emission, so + // a future addition can't silently overwrite a handler in the HashMap. + let mut seen: BTreeSet = BTreeSet::new(); + for trait_def in &traits { + for method in &trait_def.methods { + let key = wire_method_name(&trait_def.name, &method.name); + if !seen.insert(key.clone()) { + bail!( + "Wire method name `{key}` registered twice; \ + change `{}::{}` or its sibling trait to disambiguate", + trait_def.name, + method.name + ); + } + } + } + + let mut modules = Vec::with_capacity(traits.len()); + for trait_def in &traits { + modules.push(ModuleEmission::build(trait_def)?); + } + + let mut out = String::new(); + write_header(&mut out); + write_imports(&mut out, &traits); + writeln!(out).unwrap(); + write_top_register(&mut out, &traits); + + for module in &modules { + writeln!(out).unwrap(); + out.push_str(&module.code); + } + + Ok(out) +} + +/// Returns the traits to emit, in the order declared by the top-level +/// `TrUApi` super-trait. Falls back to alphabetical order if the +/// extractor did not record a public ordering (e.g. synthetic tests). +fn order_traits(api: &ApiDefinition) -> Result> { + let by_name: BTreeMap<&str, &TraitDef> = + api.traits.iter().map(|t| (t.name.as_str(), t)).collect(); + + if api.public_trait_order.is_empty() { + return Ok(api.traits.iter().collect()); + } + + let mut ordered = Vec::with_capacity(api.public_trait_order.len()); + for name in &api.public_trait_order { + let Some(trait_def) = by_name.get(name.as_str()) else { + bail!("trait `{name}` appears in TrUApi but was not extracted"); + }; + ordered.push(*trait_def); + } + Ok(ordered) +} + +struct ModuleEmission { + code: String, +} + +impl ModuleEmission { + fn build(trait_def: &TraitDef) -> Result { + let module = module_for_trait(&trait_def.name); + + let mut methods = Vec::with_capacity(trait_def.methods.len()); + for method in &trait_def.methods { + let wire_method = wire_method_name(&trait_def.name, &method.name); + methods.push(MethodEmission::build(&module, &wire_method, method)?); + } + + let fn_name = format!("register_{module}"); + let mut code = String::new(); + writeln!( + code, + "fn {fn_name}

(dispatcher: &mut Dispatcher, host: Arc

)" + ) + .unwrap(); + writeln!(code, "where").unwrap(); + writeln!(code, " P: {} + Send + Sync + 'static,", trait_def.name).unwrap(); + writeln!(code, "{{").unwrap(); + let last = methods.len().saturating_sub(1); + for (idx, method) in methods.iter().enumerate() { + let host_expr = if idx == last { "host" } else { "host.clone()" }; + method.write(&mut code, host_expr); + } + writeln!(code, "}}").unwrap(); + + Ok(ModuleEmission { code }) + } +} + +struct MethodEmission { + /// Rust method name on the host trait (used for the `host.(...)` call). + name: String, + /// Fully-qualified wire method name (`{trait_snake}_{method}`); used as the + /// dispatcher registration key and the tag prefix. + wire_name: String, + module: String, + kind: MethodKind, + request_wrapper: Option, + response_wrapper: Option, + item_wrapper: Option, +} + +impl MethodEmission { + fn build(module: &str, wire_method: &str, method: &MethodDef) -> Result { + let request_wrapper = match method.params.as_slice() { + [] => None, + [param] => match ¶m.type_ref { + TypeRef::Named { name, args } if args.is_empty() => Some(name.clone()), + _ => bail!( + "Method `{}`: expected a single versioned-wrapper request parameter", + method.name + ), + }, + _ => bail!( + "Method `{}`: expected at most one request parameter (got {})", + method.name, + method.params.len() + ), + }; + + let (response_wrapper, item_wrapper) = match &method.return_type { + // `Result<(), _>` returns produce an empty wire payload. + // The trait method is called for its side effects and the + // dispatcher encodes `()` (zero bytes) on success. + ReturnType::Result { + ok: TypeRef::Unit, .. + } => (None, None), + ReturnType::Result { ok, .. } => ( + Some(named_root(ok).ok_or_else(|| { + anyhow::anyhow!( + "Method `{}`: response is not a versioned wrapper", + method.name + ) + })?), + None, + ), + ReturnType::Subscription(item) => ( + None, + Some(named_root(item).ok_or_else(|| { + anyhow::anyhow!( + "Method `{}`: subscription item is not a versioned wrapper", + method.name + ) + })?), + ), + ReturnType::ResultSubscription { item, .. } => ( + None, + Some(named_root(item).ok_or_else(|| { + anyhow::anyhow!( + "Method `{}`: subscription item is not a versioned wrapper", + method.name + ) + })?), + ), + }; + + Ok(MethodEmission { + name: method.name.clone(), + wire_name: wire_method.to_string(), + module: module.to_string(), + kind: match method.kind { + MethodKind::Request => MethodKind::Request, + MethodKind::Subscription => MethodKind::Subscription, + MethodKind::ResultSubscription => MethodKind::ResultSubscription, + }, + request_wrapper, + response_wrapper, + item_wrapper, + }) + } + + fn write(&self, out: &mut String, host_expr: &str) { + match self.kind { + MethodKind::Request => self.write_request(out, host_expr), + MethodKind::Subscription | MethodKind::ResultSubscription => { + self.write_subscription(out, host_expr) + } + } + } + + fn write_request(&self, out: &mut String, host_expr: &str) { + let module = &self.module; + let method = &self.name; + let wire = &self.wire_name; + + writeln!(out, " {{").unwrap(); + writeln!(out, " let host = {host_expr};").unwrap(); + writeln!( + out, + " dispatcher.on_request(\"{wire}\", move |request_id: String, bytes: Vec| {{" + ) + .unwrap(); + writeln!(out, " let host = host.clone();").unwrap(); + writeln!(out, " Box::pin(async move {{").unwrap(); + let call_args = if let Some(request) = &self.request_wrapper { + writeln!( + out, + " let request: versioned::{module}::{request} =" + ) + .unwrap(); + writeln!( + out, + " Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?;" + ) + .unwrap(); + "&cx, request" + } else { + writeln!(out, " let _ = bytes;").unwrap(); + "&cx" + }; + writeln!( + out, + " let cx = CallContext::with_request_id(request_id.clone());" + ) + .unwrap(); + match &self.response_wrapper { + Some(response) => { + writeln!( + out, + " let response: versioned::{module}::{response} = match host.{method}({call_args}).await {{", + ) + .unwrap(); + writeln!(out, " Ok(value) => value,").unwrap(); + writeln!( + out, + " Err(err) => return Err(ProtocolMessage::call_error(err))," + ) + .unwrap(); + writeln!(out, " }};").unwrap(); + writeln!( + out, + " let mut buf = Vec::with_capacity(1 + response.size_hint());" + ) + .unwrap(); + writeln!(out, " buf.push(0u8);").unwrap(); + writeln!(out, " response.encode_to(&mut buf);").unwrap(); + writeln!(out, " Ok(buf)").unwrap(); + } + None => { + writeln!( + out, + " match host.{method}({call_args}).await {{" + ) + .unwrap(); + writeln!(out, " Ok(()) => Ok(vec![0u8]),").unwrap(); + writeln!( + out, + " Err(err) => Err(ProtocolMessage::call_error(err))," + ) + .unwrap(); + writeln!(out, " }}").unwrap(); + } + } + writeln!(out, " }})").unwrap(); + writeln!(out, " }});").unwrap(); + writeln!(out, " }}").unwrap(); + } + + fn write_subscription(&self, out: &mut String, host_expr: &str) { + let module = &self.module; + let method = &self.name; + let wire = &self.wire_name; + let item = self + .item_wrapper + .as_deref() + .expect("subscription methods must have an item wrapper"); + + let is_result_sub = matches!(self.kind, MethodKind::ResultSubscription); + + writeln!(out, " {{").unwrap(); + writeln!(out, " let host = {host_expr};").unwrap(); + writeln!( + out, + " dispatcher.on_subscription(\"{wire}\", move |request_id: String, bytes: Vec| {{" + ) + .unwrap(); + writeln!(out, " let host = host.clone();").unwrap(); + writeln!(out, " Box::pin(async move {{").unwrap(); + if let Some(request) = &self.request_wrapper { + writeln!( + out, + " let request: versioned::{module}::{request} =" + ) + .unwrap(); + writeln!( + out, + " Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?;" + ) + .unwrap(); + writeln!( + out, + " let cx = CallContext::with_request_id(request_id.clone());" + ) + .unwrap(); + if is_result_sub { + writeln!( + out, + " let stream = match host.{method}(&cx, request).await {{" + ) + .unwrap(); + writeln!(out, " Ok(sub) => sub,").unwrap(); + writeln!( + out, + " Err(err) => return Err(ProtocolMessage::call_error(err))," + ) + .unwrap(); + writeln!(out, " }};").unwrap(); + } else { + writeln!( + out, + " let stream = host.{method}(&cx, request).await;" + ) + .unwrap(); + } + } else { + writeln!(out, " let _ = bytes;").unwrap(); + writeln!( + out, + " let cx = CallContext::with_request_id(request_id.clone());" + ) + .unwrap(); + if is_result_sub { + writeln!( + out, + " let stream = match host.{method}(&cx).await {{" + ) + .unwrap(); + writeln!(out, " Ok(sub) => sub,").unwrap(); + writeln!( + out, + " Err(err) => return Err(ProtocolMessage::call_error(err))," + ) + .unwrap(); + writeln!(out, " }};").unwrap(); + } else { + writeln!( + out, + " let stream = host.{method}(&cx).await;" + ) + .unwrap(); + } + } + writeln!( + out, + " Ok(subscription_stream::(stream))" + ) + .unwrap(); + writeln!(out, " }})").unwrap(); + writeln!(out, " }});").unwrap(); + writeln!(out, " }}").unwrap(); + } +} + +fn named_root(ty: &TypeRef) -> Option { + if let TypeRef::Named { name, args } = ty + && args.is_empty() + { + return Some(name.clone()); + } + None +} + +fn write_header(out: &mut String) { + writeln!(out, "//! Wire dispatcher for the unified `TrUApi` trait.").unwrap(); + writeln!(out, "//!").unwrap(); + writeln!(out, "//! Auto-generated by truapi-codegen. Do not edit.").unwrap(); + writeln!(out).unwrap(); +} + +fn write_imports(out: &mut String, traits: &[&TraitDef]) { + writeln!(out, "use std::sync::Arc;").unwrap(); + writeln!(out).unwrap(); + writeln!(out, "use parity_scale_codec::{{Decode, Encode}};").unwrap(); + writeln!(out).unwrap(); + writeln!(out, "use truapi::CallContext;").unwrap(); + writeln!(out, "use truapi::api::{{").unwrap(); + for trait_def in traits { + writeln!(out, " {},", trait_def.name).unwrap(); + } + writeln!(out, "}};").unwrap(); + writeln!(out, "use truapi::versioned;").unwrap(); + writeln!(out).unwrap(); + writeln!(out, "use crate::dispatcher::Dispatcher;").unwrap(); + writeln!(out, "use crate::frame::ProtocolMessage;").unwrap(); + writeln!(out, "use crate::subscription::subscription_stream;").unwrap(); +} + +fn write_top_register(out: &mut String, traits: &[&TraitDef]) { + writeln!(out, "/// Register every TrUAPI method with the dispatcher.").unwrap(); + writeln!( + out, + "pub fn register

(dispatcher: &mut Dispatcher, host: Arc

)" + ) + .unwrap(); + writeln!(out, "where").unwrap(); + let trait_names: Vec<&str> = traits.iter().map(|t| t.name.as_str()).collect(); + let bounds = trait_names.join(" + "); + writeln!(out, " P: {bounds} + Send + Sync + 'static,").unwrap(); + writeln!(out, "{{").unwrap(); + let last = traits.len().saturating_sub(1); + for (idx, trait_def) in traits.iter().enumerate() { + let host_expr = if idx == last { "host" } else { "host.clone()" }; + let module = module_for_trait(&trait_def.name); + writeln!(out, " register_{module}(dispatcher, {host_expr});").unwrap(); + } + writeln!(out, "}}").unwrap(); +} diff --git a/rust/crates/truapi-codegen/src/rust/wire_table.rs b/rust/crates/truapi-codegen/src/rust/wire_table.rs new file mode 100644 index 00000000..430a3a11 --- /dev/null +++ b/rust/crates/truapi-codegen/src/rust/wire_table.rs @@ -0,0 +1,287 @@ +//! Emits `wire_table.rs`: the (id, tag) lookup table the server uses to +//! pair incoming wire frames with their request, response, or +//! subscription role. +//! +//! Per-method `#[wire(...)]` annotations decide id assignment: +//! - request methods reserve `(request_id, response_id)`. +//! - subscription methods reserve `(start_id, stop_id, interrupt_id, receive_id)`. +//! +//! Missing annotations and collisions both hard-fail codegen. + +use std::collections::BTreeMap; +use std::fmt::Write; + +use anyhow::{Result, bail}; + +use crate::rustdoc::*; + +use super::wire_method_name; + +#[derive(Debug, Clone, Copy)] +struct WireEntry { + request_id: u8, + response_id: u8, +} + +#[derive(Debug, Clone, Copy)] +struct SubEntry { + start_id: u8, + stop_id: u8, + interrupt_id: u8, + receive_id: u8, +} + +#[derive(Debug, Clone, Copy)] +enum MethodEntry { + Request(WireEntry), + Subscription(SubEntry), +} + +/// Emit the contents of `wire_table.rs`. +pub fn generate_wire_table(api: &ApiDefinition) -> Result { + let mut method_entries: Vec<(String, MethodEntry)> = Vec::new(); + let mut seen: BTreeMap = BTreeMap::new(); + let mut seen_methods: BTreeMap = BTreeMap::new(); + + for trait_def in &api.traits { + for method in &trait_def.methods { + let entry = method_entry(trait_def, method)?; + let wire_method = wire_method_name(&trait_def.name, &method.name); + if let Some(existing) = seen_methods.insert( + wire_method.clone(), + format!("{}::{}", trait_def.name, method.name), + ) { + bail!( + "wire method name `{wire_method}` reused: `{existing}` and `{}::{}` collide", + trait_def.name, + method.name + ); + } + insert_entry(&mut seen, &wire_method, entry)?; + method_entries.push((wire_method, entry)); + } + } + + method_entries.sort_by_key(|(_, entry)| match entry { + MethodEntry::Request(WireEntry { request_id, .. }) => *request_id, + MethodEntry::Subscription(SubEntry { start_id, .. }) => *start_id, + }); + + render(&method_entries, &seen) +} + +fn method_entry(trait_def: &TraitDef, method: &MethodDef) -> Result { + let wire = &method.wire; + match method.kind { + MethodKind::Request => { + if wire.start_id.is_some() + || wire.stop_id.is_some() + || wire.interrupt_id.is_some() + || wire.receive_id.is_some() + { + bail!( + "method `{}::{}` is a request and must not use subscription wire ids", + trait_def.name, + method.name + ); + } + let request_id = wire.request_id.ok_or_else(|| { + anyhow::anyhow!( + "method `{}::{}` is missing #[wire(request_id = N)] annotation", + trait_def.name, + method.name + ) + })?; + let response_id = infer_id(wire.response_id, request_id, 1, &method.name)?; + Ok(MethodEntry::Request(WireEntry { + request_id, + response_id, + })) + } + MethodKind::Subscription | MethodKind::ResultSubscription => { + if wire.request_id.is_some() || wire.response_id.is_some() { + bail!( + "method `{}::{}` is a subscription and must not use request wire ids", + trait_def.name, + method.name + ); + } + let start_id = wire.start_id.ok_or_else(|| { + anyhow::anyhow!( + "method `{}::{}` is missing #[wire(start_id = N)] annotation", + trait_def.name, + method.name + ) + })?; + let stop_id = infer_id(wire.stop_id, start_id, 1, &method.name)?; + let interrupt_id = infer_id(wire.interrupt_id, start_id, 2, &method.name)?; + let receive_id = infer_id(wire.receive_id, start_id, 3, &method.name)?; + Ok(MethodEntry::Subscription(SubEntry { + start_id, + stop_id, + interrupt_id, + receive_id, + })) + } + } +} + +fn infer_id(explicit: Option, anchor: u8, offset: u8, method_name: &str) -> Result { + if let Some(id) = explicit { + return Ok(id); + } + anchor + .checked_add(offset) + .ok_or_else(|| anyhow::anyhow!("wire id overflow on `{method_name}` (base {anchor})")) +} + +fn insert_entry( + seen: &mut BTreeMap, + method_name: &str, + entry: MethodEntry, +) -> Result<()> { + let pairs: Vec<(u8, String)> = match entry { + MethodEntry::Request(WireEntry { + request_id, + response_id, + }) => vec![ + (request_id, format!("{method_name}_request")), + (response_id, format!("{method_name}_response")), + ], + MethodEntry::Subscription(SubEntry { + start_id, + stop_id, + interrupt_id, + receive_id, + }) => vec![ + (start_id, format!("{method_name}_start")), + (stop_id, format!("{method_name}_stop")), + (interrupt_id, format!("{method_name}_interrupt")), + (receive_id, format!("{method_name}_receive")), + ], + }; + for (id, tag) in pairs { + if let Some(existing) = seen.insert(id, tag.clone()) { + bail!("wire id {id} reused: `{existing}` and `{tag}` collide"); + } + } + Ok(()) +} + +fn render(methods: &[(String, MethodEntry)], seen: &BTreeMap) -> Result { + let mut out = String::new(); + writeln!(out, "//! Wire-protocol discriminant table.").unwrap(); + writeln!(out, "//!").unwrap(); + writeln!(out, "//! Auto-generated by truapi-codegen. Do not edit.").unwrap(); + writeln!(out, "//!").unwrap(); + writeln!( + out, + "//! Each method reserves either two ids (request/response) or four" + ) + .unwrap(); + writeln!( + out, + "//! (start/stop/interrupt/receive). The table is sorted by request/start id." + ) + .unwrap(); + writeln!(out).unwrap(); + + // Embed an audit comment block: one `// entry id= tag=""` + // line per discriminant, sorted by id. This is what the codegen + // unit tests consume as a parseable shadow of the slice below. + for (id, tag) in seen { + writeln!(out, "// entry id={id} tag=\"{tag}\"").unwrap(); + } + writeln!(out).unwrap(); + + writeln!(out, "/// A single wire-table row.").unwrap(); + writeln!(out, "pub struct WireEntry {{").unwrap(); + writeln!(out, " /// Method name from the Rust trait.").unwrap(); + writeln!(out, " pub method: &'static str,").unwrap(); + writeln!(out, " /// What kind of slot this entry describes.").unwrap(); + writeln!(out, " pub kind: WireKind,").unwrap(); + writeln!(out, "}}").unwrap(); + writeln!(out).unwrap(); + + writeln!( + out, + "/// Wire-slot shape: request/response pair or subscription quartet." + ) + .unwrap(); + writeln!(out, "pub enum WireKind {{").unwrap(); + writeln!(out, " /// Request/response method.").unwrap(); + writeln!(out, " Request {{").unwrap(); + writeln!(out, " /// Discriminant for the request frame.").unwrap(); + writeln!(out, " request_id: u8,").unwrap(); + writeln!(out, " /// Discriminant for the response frame.").unwrap(); + writeln!(out, " response_id: u8,").unwrap(); + writeln!(out, " }},").unwrap(); + writeln!(out, " /// Subscription method.").unwrap(); + writeln!(out, " Subscription {{").unwrap(); + writeln!(out, " /// Discriminant for the start frame.").unwrap(); + writeln!(out, " start_id: u8,").unwrap(); + writeln!(out, " /// Discriminant for the stop frame.").unwrap(); + writeln!(out, " stop_id: u8,").unwrap(); + writeln!( + out, + " /// Discriminant for the interrupt frame (server-initiated termination)." + ) + .unwrap(); + writeln!(out, " interrupt_id: u8,").unwrap(); + writeln!( + out, + " /// Discriminant for each receive frame (a streamed item)." + ) + .unwrap(); + writeln!(out, " receive_id: u8,").unwrap(); + writeln!(out, " }},").unwrap(); + writeln!(out, "}}").unwrap(); + writeln!(out).unwrap(); + + writeln!( + out, + "/// The full wire table. Ordering is part of the wire protocol;" + ) + .unwrap(); + writeln!( + out, + "/// only ever append. Removed methods leave their slot empty." + ) + .unwrap(); + writeln!(out, "pub const WIRE_TABLE: &[WireEntry] = &[").unwrap(); + for (name, entry) in methods { + match entry { + MethodEntry::Request(WireEntry { + request_id, + response_id, + }) => { + writeln!(out, " WireEntry {{").unwrap(); + writeln!(out, " method: \"{name}\",").unwrap(); + writeln!(out, " kind: WireKind::Request {{").unwrap(); + writeln!(out, " request_id: {request_id},").unwrap(); + writeln!(out, " response_id: {response_id},").unwrap(); + writeln!(out, " }},").unwrap(); + writeln!(out, " }},").unwrap(); + } + MethodEntry::Subscription(SubEntry { + start_id, + stop_id, + interrupt_id, + receive_id, + }) => { + writeln!(out, " WireEntry {{").unwrap(); + writeln!(out, " method: \"{name}\",").unwrap(); + writeln!(out, " kind: WireKind::Subscription {{").unwrap(); + writeln!(out, " start_id: {start_id},").unwrap(); + writeln!(out, " stop_id: {stop_id},").unwrap(); + writeln!(out, " interrupt_id: {interrupt_id},").unwrap(); + writeln!(out, " receive_id: {receive_id},").unwrap(); + writeln!(out, " }},").unwrap(); + writeln!(out, " }},").unwrap(); + } + } + } + writeln!(out, "];").unwrap(); + + Ok(out) +} diff --git a/rust/crates/truapi-codegen/tests/fixtures/sample.rustdoc.json b/rust/crates/truapi-codegen/tests/fixtures/sample.rustdoc.json new file mode 100644 index 00000000..8de55a6a --- /dev/null +++ b/rust/crates/truapi-codegen/tests/fixtures/sample.rustdoc.json @@ -0,0 +1,3 @@ +{ + "_comment": "Placeholder fixture: the golden test drives the binary against the truapi crate's real rustdoc JSON (regenerated at test time). Checking in a 12 MB rustdoc dump would dwarf the rest of the repo; the test instead invokes `cargo +nightly rustdoc -p truapi` and parses the result. This file exists so the path referenced in the test layout is real." +} diff --git a/rust/crates/truapi-codegen/tests/golden/dispatcher.rs b/rust/crates/truapi-codegen/tests/golden/dispatcher.rs new file mode 100644 index 00000000..8ae65341 --- /dev/null +++ b/rust/crates/truapi-codegen/tests/golden/dispatcher.rs @@ -0,0 +1,1131 @@ +//! Wire dispatcher for the unified `TrUApi` trait. +//! +//! Auto-generated by truapi-codegen. Do not edit. + +use std::sync::Arc; + +use parity_scale_codec::{Decode, Encode}; + +use truapi::CallContext; +use truapi::api::{ + Account, + Chain, + Chat, + Entropy, + JsonRpc, + LocalStorage, + Payment, + Permissions, + Preimage, + ResourceAllocation, + Signing, + StatementStore, + System, + Theme, +}; +use truapi::versioned; + +use crate::dispatcher::Dispatcher; +use crate::frame::ProtocolMessage; +use crate::subscription::subscription_stream; + +/// Register every TrUAPI method with the dispatcher. +pub fn register

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Account + Chain + Chat + Entropy + JsonRpc + LocalStorage + Payment + Permissions + Preimage + ResourceAllocation + Signing + StatementStore + System + Theme + Send + Sync + 'static, +{ + register_account(dispatcher, host.clone()); + register_chain(dispatcher, host.clone()); + register_chat(dispatcher, host.clone()); + register_entropy(dispatcher, host.clone()); + register_jsonrpc(dispatcher, host.clone()); + register_local_storage(dispatcher, host.clone()); + register_payment(dispatcher, host.clone()); + register_permissions(dispatcher, host.clone()); + register_preimage(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); +} + +fn register_account

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Account + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription("account_connection_status_subscribe", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let _ = bytes; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.connection_status_subscribe(&cx).await; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("account_get_account", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostAccountGetRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostAccountGetResponse = match host.get_account(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("account_get_account_alias", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostAccountGetAliasRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostAccountGetAliasResponse = match host.get_account_alias(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("account_create_account_proof", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostAccountCreateProofRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostAccountCreateProofResponse = match host.create_account_proof(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("account_get_legacy_accounts", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostGetLegacyAccountsRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostGetLegacyAccountsResponse = match host.get_legacy_accounts(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("account_get_user_id", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostGetUserIdRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostGetUserIdResponse = match host.get_user_id(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host; + dispatcher.on_request("account_request_login", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostRequestLoginRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostRequestLoginResponse = match host.request_login(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } +} + +fn register_chain

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Chain + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription("chain_follow_head_subscribe", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadFollowRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.follow_head_subscribe(&cx, request).await; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("chain_get_head_header", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadHeaderRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadHeaderResponse = match host.get_head_header(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("chain_get_head_body", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadBodyRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadBodyResponse = match host.get_head_body(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("chain_get_head_storage", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadStorageRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadStorageResponse = match host.get_head_storage(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("chain_call_head", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadCallRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadCallResponse = match host.call_head(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("chain_unpin_head", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadUnpinRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadUnpinResponse = match host.unpin_head(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("chain_continue_head", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadContinueRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadContinueResponse = match host.continue_head(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("chain_stop_head_operation", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadStopOperationRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadStopOperationResponse = match host.stop_head_operation(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("chain_get_spec_genesis_hash", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainSpecGenesisHashRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainSpecGenesisHashResponse = match host.get_spec_genesis_hash(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("chain_get_spec_chain_name", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainSpecChainNameRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainSpecChainNameResponse = match host.get_spec_chain_name(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("chain_get_spec_properties", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainSpecPropertiesRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainSpecPropertiesResponse = match host.get_spec_properties(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("chain_broadcast_transaction", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainTransactionBroadcastRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainTransactionBroadcastResponse = match host.broadcast_transaction(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host; + dispatcher.on_request("chain_stop_transaction", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainTransactionStopRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainTransactionStopResponse = match host.stop_transaction(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } +} + +fn register_chat

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Chat + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request("chat_create_room", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chat::HostChatCreateRoomRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chat::HostChatCreateRoomResponse = match host.create_room(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("chat_register_bot", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chat::HostChatRegisterBotRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chat::HostChatRegisterBotResponse = match host.register_bot(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_subscription("chat_list_subscribe", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let _ = bytes; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.list_subscribe(&cx).await; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("chat_post_message", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chat::HostChatPostMessageRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chat::HostChatPostMessageResponse = match host.post_message(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_subscription("chat_action_subscribe", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let _ = bytes; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.action_subscribe(&cx).await; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host; + dispatcher.on_subscription("chat_custom_message_render_subscribe", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chat::ProductChatCustomMessageRenderSubscribeRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.custom_message_render_subscribe(&cx, request).await; + Ok(subscription_stream::(stream)) + }) + }); + } +} + +fn register_entropy

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Entropy + Send + Sync + 'static, +{ + { + let host = host; + dispatcher.on_request("entropy_derive", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::entropy::HostDeriveEntropyRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::entropy::HostDeriveEntropyResponse = match host.derive(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } +} + +fn register_jsonrpc

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: JsonRpc + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request("json_rpc_send_message", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::jsonrpc::HostJsonrpcMessageSendRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::jsonrpc::HostJsonrpcMessageSendResponse = match host.send_message(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host; + dispatcher.on_subscription("json_rpc_subscribe_messages", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::jsonrpc::HostJsonrpcMessageSubscribeRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.subscribe_messages(&cx, request).await; + Ok(subscription_stream::(stream)) + }) + }); + } +} + +fn register_local_storage

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: LocalStorage + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request("local_storage_read", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::local_storage::HostLocalStorageReadRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::local_storage::HostLocalStorageReadResponse = match host.read(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("local_storage_write", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::local_storage::HostLocalStorageWriteRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::local_storage::HostLocalStorageWriteResponse = match host.write(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host; + dispatcher.on_request("local_storage_clear", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::local_storage::HostLocalStorageClearRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::local_storage::HostLocalStorageClearResponse = match host.clear(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } +} + +fn register_payment

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Payment + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription("payment_balance_subscribe", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::payment::HostPaymentBalanceSubscribeRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.balance_subscribe(&cx, request).await { + Ok(sub) => sub, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("payment_request", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::payment::HostPaymentRequestRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::payment::HostPaymentRequestResponse = match host.request(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_subscription("payment_status_subscribe", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::payment::HostPaymentStatusSubscribeRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.status_subscribe(&cx, request).await { + Ok(sub) => sub, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host; + dispatcher.on_request("payment_top_up", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::payment::HostPaymentTopUpRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::payment::HostPaymentTopUpResponse = match host.top_up(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } +} + +fn register_permissions

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Permissions + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request("permissions_request_device_permission", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::permissions::HostDevicePermissionRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::permissions::HostDevicePermissionResponse = match host.request_device_permission(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host; + dispatcher.on_request("permissions_request_remote_permission", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::permissions::RemotePermissionRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::permissions::RemotePermissionResponse = match host.request_remote_permission(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } +} + +fn register_preimage

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Preimage + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription("preimage_lookup_subscribe", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::preimage::RemotePreimageLookupSubscribeRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.lookup_subscribe(&cx, request).await; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host; + dispatcher.on_request("preimage_submit", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::preimage::RemotePreimageSubmitRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::preimage::RemotePreimageSubmitResponse = match host.submit(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } +} + +fn register_resource_allocation

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: ResourceAllocation + Send + Sync + 'static, +{ + { + let host = host; + dispatcher.on_request("resource_allocation_request", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::resource_allocation::HostRequestResourceAllocationRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::resource_allocation::HostRequestResourceAllocationResponse = match host.request(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } +} + +fn register_signing

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Signing + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request("signing_create_transaction", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostCreateTransactionRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostCreateTransactionResponse = match host.create_transaction(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("signing_create_transaction_with_legacy_account", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostCreateTransactionWithLegacyAccountRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostCreateTransactionWithLegacyAccountResponse = match host.create_transaction_with_legacy_account(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("signing_sign_raw_with_legacy_account", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostSignRawWithLegacyAccountRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostSignRawWithLegacyAccountResponse = match host.sign_raw_with_legacy_account(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("signing_sign_payload_with_legacy_account", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostSignPayloadWithLegacyAccountRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostSignPayloadWithLegacyAccountResponse = match host.sign_payload_with_legacy_account(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("signing_sign_raw", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostSignRawRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostSignRawResponse = match host.sign_raw(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host; + dispatcher.on_request("signing_sign_payload", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostSignPayloadRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostSignPayloadResponse = match host.sign_payload(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } +} + +fn register_statement_store

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: StatementStore + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription("statement_store_subscribe", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::statement_store::RemoteStatementStoreSubscribeRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.subscribe(&cx, request).await; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("statement_store_create_proof", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::statement_store::RemoteStatementStoreCreateProofRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::statement_store::RemoteStatementStoreCreateProofResponse = match host.create_proof(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("statement_store_create_proof_authorized", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedResponse = match host.create_proof_authorized(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host; + dispatcher.on_request("statement_store_submit", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::statement_store::RemoteStatementStoreSubmitRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + match host.submit(&cx, request).await { + Ok(()) => Ok(vec![0u8]), + Err(err) => Err(ProtocolMessage::call_error(err)), + } + }) + }); + } +} + +fn register_system

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: System + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request("system_handshake", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::system::HostHandshakeRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::system::HostHandshakeResponse = match host.handshake(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("system_feature_supported", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::system::HostFeatureSupportedRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::system::HostFeatureSupportedResponse = match host.feature_supported(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("system_push_notification", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::system::HostPushNotificationRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::system::HostPushNotificationResponse = match host.push_notification(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host; + dispatcher.on_request("system_navigate_to", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::system::HostNavigateToRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::system::HostNavigateToResponse = match host.navigate_to(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } +} + +fn register_theme

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Theme + Send + Sync + 'static, +{ + { + let host = host; + dispatcher.on_subscription("theme_subscribe", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let _ = bytes; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.subscribe(&cx).await; + Ok(subscription_stream::(stream)) + }) + }); + } +} diff --git a/rust/crates/truapi-codegen/tests/golden/wire_table.rs b/rust/crates/truapi-codegen/tests/golden/wire_table.rs new file mode 100644 index 00000000..d524e2bf --- /dev/null +++ b/rust/crates/truapi-codegen/tests/golden/wire_table.rs @@ -0,0 +1,590 @@ +//! Wire-protocol discriminant table. +//! +//! Auto-generated by truapi-codegen. Do not edit. +//! +//! Each method reserves either two ids (request/response) or four +//! (start/stop/interrupt/receive). The table is sorted by request/start id. + +// entry id=0 tag="system_handshake_request" +// entry id=1 tag="system_handshake_response" +// entry id=2 tag="system_feature_supported_request" +// entry id=3 tag="system_feature_supported_response" +// entry id=4 tag="system_push_notification_request" +// entry id=5 tag="system_push_notification_response" +// entry id=6 tag="system_navigate_to_request" +// entry id=7 tag="system_navigate_to_response" +// entry id=8 tag="permissions_request_device_permission_request" +// entry id=9 tag="permissions_request_device_permission_response" +// entry id=10 tag="permissions_request_remote_permission_request" +// entry id=11 tag="permissions_request_remote_permission_response" +// entry id=12 tag="local_storage_read_request" +// entry id=13 tag="local_storage_read_response" +// entry id=14 tag="local_storage_write_request" +// entry id=15 tag="local_storage_write_response" +// entry id=16 tag="local_storage_clear_request" +// entry id=17 tag="local_storage_clear_response" +// entry id=18 tag="account_connection_status_subscribe_start" +// entry id=19 tag="account_connection_status_subscribe_stop" +// entry id=20 tag="account_connection_status_subscribe_interrupt" +// entry id=21 tag="account_connection_status_subscribe_receive" +// entry id=22 tag="account_get_account_request" +// entry id=23 tag="account_get_account_response" +// entry id=24 tag="account_get_account_alias_request" +// entry id=25 tag="account_get_account_alias_response" +// entry id=26 tag="account_create_account_proof_request" +// entry id=27 tag="account_create_account_proof_response" +// entry id=28 tag="account_get_legacy_accounts_request" +// entry id=29 tag="account_get_legacy_accounts_response" +// entry id=30 tag="signing_create_transaction_request" +// entry id=31 tag="signing_create_transaction_response" +// entry id=32 tag="signing_create_transaction_with_legacy_account_request" +// entry id=33 tag="signing_create_transaction_with_legacy_account_response" +// entry id=34 tag="signing_sign_raw_with_legacy_account_request" +// entry id=35 tag="signing_sign_raw_with_legacy_account_response" +// entry id=36 tag="signing_sign_payload_with_legacy_account_request" +// entry id=37 tag="signing_sign_payload_with_legacy_account_response" +// entry id=38 tag="chat_create_room_request" +// entry id=39 tag="chat_create_room_response" +// entry id=40 tag="chat_register_bot_request" +// entry id=41 tag="chat_register_bot_response" +// entry id=42 tag="chat_list_subscribe_start" +// entry id=43 tag="chat_list_subscribe_stop" +// entry id=44 tag="chat_list_subscribe_interrupt" +// entry id=45 tag="chat_list_subscribe_receive" +// entry id=46 tag="chat_post_message_request" +// entry id=47 tag="chat_post_message_response" +// entry id=48 tag="chat_action_subscribe_start" +// entry id=49 tag="chat_action_subscribe_stop" +// entry id=50 tag="chat_action_subscribe_interrupt" +// entry id=51 tag="chat_action_subscribe_receive" +// entry id=52 tag="chat_custom_message_render_subscribe_start" +// entry id=53 tag="chat_custom_message_render_subscribe_stop" +// entry id=54 tag="chat_custom_message_render_subscribe_interrupt" +// entry id=55 tag="chat_custom_message_render_subscribe_receive" +// entry id=56 tag="statement_store_subscribe_start" +// entry id=57 tag="statement_store_subscribe_stop" +// entry id=58 tag="statement_store_subscribe_interrupt" +// entry id=59 tag="statement_store_subscribe_receive" +// entry id=60 tag="statement_store_create_proof_request" +// entry id=61 tag="statement_store_create_proof_response" +// entry id=62 tag="statement_store_submit_request" +// entry id=63 tag="statement_store_submit_response" +// entry id=64 tag="preimage_lookup_subscribe_start" +// entry id=65 tag="preimage_lookup_subscribe_stop" +// entry id=66 tag="preimage_lookup_subscribe_interrupt" +// entry id=67 tag="preimage_lookup_subscribe_receive" +// entry id=68 tag="preimage_submit_request" +// entry id=69 tag="preimage_submit_response" +// entry id=70 tag="json_rpc_send_message_request" +// entry id=71 tag="json_rpc_send_message_response" +// entry id=72 tag="json_rpc_subscribe_messages_start" +// entry id=73 tag="json_rpc_subscribe_messages_stop" +// entry id=74 tag="json_rpc_subscribe_messages_interrupt" +// entry id=75 tag="json_rpc_subscribe_messages_receive" +// entry id=76 tag="chain_follow_head_subscribe_start" +// entry id=77 tag="chain_follow_head_subscribe_stop" +// entry id=78 tag="chain_follow_head_subscribe_interrupt" +// entry id=79 tag="chain_follow_head_subscribe_receive" +// entry id=80 tag="chain_get_head_header_request" +// entry id=81 tag="chain_get_head_header_response" +// entry id=82 tag="chain_get_head_body_request" +// entry id=83 tag="chain_get_head_body_response" +// entry id=84 tag="chain_get_head_storage_request" +// entry id=85 tag="chain_get_head_storage_response" +// entry id=86 tag="chain_call_head_request" +// entry id=87 tag="chain_call_head_response" +// entry id=88 tag="chain_unpin_head_request" +// entry id=89 tag="chain_unpin_head_response" +// entry id=90 tag="chain_continue_head_request" +// entry id=91 tag="chain_continue_head_response" +// entry id=92 tag="chain_stop_head_operation_request" +// entry id=93 tag="chain_stop_head_operation_response" +// entry id=94 tag="chain_get_spec_genesis_hash_request" +// entry id=95 tag="chain_get_spec_genesis_hash_response" +// entry id=96 tag="chain_get_spec_chain_name_request" +// entry id=97 tag="chain_get_spec_chain_name_response" +// entry id=98 tag="chain_get_spec_properties_request" +// entry id=99 tag="chain_get_spec_properties_response" +// entry id=100 tag="chain_broadcast_transaction_request" +// entry id=101 tag="chain_broadcast_transaction_response" +// entry id=102 tag="chain_stop_transaction_request" +// entry id=103 tag="chain_stop_transaction_response" +// entry id=104 tag="theme_subscribe_start" +// entry id=105 tag="theme_subscribe_stop" +// entry id=106 tag="theme_subscribe_interrupt" +// entry id=107 tag="theme_subscribe_receive" +// entry id=108 tag="entropy_derive_request" +// entry id=109 tag="entropy_derive_response" +// entry id=110 tag="account_get_user_id_request" +// entry id=111 tag="account_get_user_id_response" +// entry id=112 tag="account_request_login_request" +// entry id=113 tag="account_request_login_response" +// entry id=114 tag="signing_sign_raw_request" +// entry id=115 tag="signing_sign_raw_response" +// entry id=116 tag="signing_sign_payload_request" +// entry id=117 tag="signing_sign_payload_response" +// entry id=118 tag="payment_balance_subscribe_start" +// entry id=119 tag="payment_balance_subscribe_stop" +// entry id=120 tag="payment_balance_subscribe_interrupt" +// entry id=121 tag="payment_balance_subscribe_receive" +// entry id=122 tag="payment_top_up_request" +// entry id=123 tag="payment_top_up_response" +// entry id=124 tag="payment_request_request" +// entry id=125 tag="payment_request_response" +// entry id=126 tag="payment_status_subscribe_start" +// entry id=127 tag="payment_status_subscribe_stop" +// entry id=128 tag="payment_status_subscribe_interrupt" +// entry id=129 tag="payment_status_subscribe_receive" +// entry id=130 tag="resource_allocation_request_request" +// entry id=131 tag="resource_allocation_request_response" +// entry id=132 tag="statement_store_create_proof_authorized_request" +// entry id=133 tag="statement_store_create_proof_authorized_response" + +/// 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 pair or subscription quartet. +pub enum WireKind { + /// Request/response method. + Request { + /// Discriminant for the request frame. + request_id: u8, + /// Discriminant for the response frame. + response_id: u8, + }, + /// Subscription method. + Subscription { + /// Discriminant for the start frame. + start_id: u8, + /// Discriminant for the stop frame. + stop_id: u8, + /// Discriminant for the interrupt frame (server-initiated termination). + interrupt_id: u8, + /// Discriminant for each receive frame (a streamed item). + receive_id: u8, + }, +} + +/// The full wire table. Ordering is part of the wire protocol; +/// only ever append. Removed methods leave their slot empty. +pub const WIRE_TABLE: &[WireEntry] = &[ + WireEntry { + method: "system_handshake", + kind: WireKind::Request { + request_id: 0, + response_id: 1, + }, + }, + WireEntry { + method: "system_feature_supported", + kind: WireKind::Request { + request_id: 2, + response_id: 3, + }, + }, + WireEntry { + method: "system_push_notification", + kind: WireKind::Request { + request_id: 4, + response_id: 5, + }, + }, + WireEntry { + method: "system_navigate_to", + kind: WireKind::Request { + request_id: 6, + response_id: 7, + }, + }, + WireEntry { + method: "permissions_request_device_permission", + kind: WireKind::Request { + request_id: 8, + response_id: 9, + }, + }, + WireEntry { + method: "permissions_request_remote_permission", + kind: WireKind::Request { + request_id: 10, + response_id: 11, + }, + }, + WireEntry { + method: "local_storage_read", + kind: WireKind::Request { + request_id: 12, + response_id: 13, + }, + }, + WireEntry { + method: "local_storage_write", + kind: WireKind::Request { + request_id: 14, + response_id: 15, + }, + }, + WireEntry { + method: "local_storage_clear", + kind: WireKind::Request { + request_id: 16, + response_id: 17, + }, + }, + WireEntry { + method: "account_connection_status_subscribe", + kind: WireKind::Subscription { + start_id: 18, + stop_id: 19, + interrupt_id: 20, + receive_id: 21, + }, + }, + WireEntry { + method: "account_get_account", + kind: WireKind::Request { + request_id: 22, + response_id: 23, + }, + }, + WireEntry { + method: "account_get_account_alias", + kind: WireKind::Request { + request_id: 24, + response_id: 25, + }, + }, + WireEntry { + method: "account_create_account_proof", + kind: WireKind::Request { + request_id: 26, + response_id: 27, + }, + }, + WireEntry { + method: "account_get_legacy_accounts", + kind: WireKind::Request { + request_id: 28, + response_id: 29, + }, + }, + WireEntry { + method: "signing_create_transaction", + kind: WireKind::Request { + request_id: 30, + response_id: 31, + }, + }, + WireEntry { + method: "signing_create_transaction_with_legacy_account", + kind: WireKind::Request { + request_id: 32, + response_id: 33, + }, + }, + WireEntry { + method: "signing_sign_raw_with_legacy_account", + kind: WireKind::Request { + request_id: 34, + response_id: 35, + }, + }, + WireEntry { + method: "signing_sign_payload_with_legacy_account", + kind: WireKind::Request { + request_id: 36, + response_id: 37, + }, + }, + WireEntry { + method: "chat_create_room", + kind: WireKind::Request { + request_id: 38, + response_id: 39, + }, + }, + WireEntry { + method: "chat_register_bot", + kind: WireKind::Request { + request_id: 40, + response_id: 41, + }, + }, + WireEntry { + method: "chat_list_subscribe", + kind: WireKind::Subscription { + start_id: 42, + stop_id: 43, + interrupt_id: 44, + receive_id: 45, + }, + }, + WireEntry { + method: "chat_post_message", + kind: WireKind::Request { + request_id: 46, + response_id: 47, + }, + }, + WireEntry { + method: "chat_action_subscribe", + kind: WireKind::Subscription { + start_id: 48, + stop_id: 49, + interrupt_id: 50, + receive_id: 51, + }, + }, + WireEntry { + method: "chat_custom_message_render_subscribe", + kind: WireKind::Subscription { + start_id: 52, + stop_id: 53, + interrupt_id: 54, + receive_id: 55, + }, + }, + WireEntry { + method: "statement_store_subscribe", + kind: WireKind::Subscription { + start_id: 56, + stop_id: 57, + interrupt_id: 58, + receive_id: 59, + }, + }, + WireEntry { + method: "statement_store_create_proof", + kind: WireKind::Request { + request_id: 60, + response_id: 61, + }, + }, + WireEntry { + method: "statement_store_submit", + kind: WireKind::Request { + request_id: 62, + response_id: 63, + }, + }, + WireEntry { + method: "preimage_lookup_subscribe", + kind: WireKind::Subscription { + start_id: 64, + stop_id: 65, + interrupt_id: 66, + receive_id: 67, + }, + }, + WireEntry { + method: "preimage_submit", + kind: WireKind::Request { + request_id: 68, + response_id: 69, + }, + }, + WireEntry { + method: "json_rpc_send_message", + kind: WireKind::Request { + request_id: 70, + response_id: 71, + }, + }, + WireEntry { + method: "json_rpc_subscribe_messages", + kind: WireKind::Subscription { + start_id: 72, + stop_id: 73, + interrupt_id: 74, + receive_id: 75, + }, + }, + WireEntry { + method: "chain_follow_head_subscribe", + kind: WireKind::Subscription { + start_id: 76, + stop_id: 77, + interrupt_id: 78, + receive_id: 79, + }, + }, + WireEntry { + method: "chain_get_head_header", + kind: WireKind::Request { + request_id: 80, + response_id: 81, + }, + }, + WireEntry { + method: "chain_get_head_body", + kind: WireKind::Request { + request_id: 82, + response_id: 83, + }, + }, + WireEntry { + method: "chain_get_head_storage", + kind: WireKind::Request { + request_id: 84, + response_id: 85, + }, + }, + WireEntry { + method: "chain_call_head", + kind: WireKind::Request { + request_id: 86, + response_id: 87, + }, + }, + WireEntry { + method: "chain_unpin_head", + kind: WireKind::Request { + request_id: 88, + response_id: 89, + }, + }, + WireEntry { + method: "chain_continue_head", + kind: WireKind::Request { + request_id: 90, + response_id: 91, + }, + }, + WireEntry { + method: "chain_stop_head_operation", + kind: WireKind::Request { + request_id: 92, + response_id: 93, + }, + }, + WireEntry { + method: "chain_get_spec_genesis_hash", + kind: WireKind::Request { + request_id: 94, + response_id: 95, + }, + }, + WireEntry { + method: "chain_get_spec_chain_name", + kind: WireKind::Request { + request_id: 96, + response_id: 97, + }, + }, + WireEntry { + method: "chain_get_spec_properties", + kind: WireKind::Request { + request_id: 98, + response_id: 99, + }, + }, + WireEntry { + method: "chain_broadcast_transaction", + kind: WireKind::Request { + request_id: 100, + response_id: 101, + }, + }, + WireEntry { + method: "chain_stop_transaction", + kind: WireKind::Request { + request_id: 102, + response_id: 103, + }, + }, + WireEntry { + method: "theme_subscribe", + kind: WireKind::Subscription { + start_id: 104, + stop_id: 105, + interrupt_id: 106, + receive_id: 107, + }, + }, + WireEntry { + method: "entropy_derive", + kind: WireKind::Request { + request_id: 108, + response_id: 109, + }, + }, + WireEntry { + method: "account_get_user_id", + kind: WireKind::Request { + request_id: 110, + response_id: 111, + }, + }, + WireEntry { + method: "account_request_login", + kind: WireKind::Request { + request_id: 112, + response_id: 113, + }, + }, + WireEntry { + method: "signing_sign_raw", + kind: WireKind::Request { + request_id: 114, + response_id: 115, + }, + }, + WireEntry { + method: "signing_sign_payload", + kind: WireKind::Request { + request_id: 116, + response_id: 117, + }, + }, + WireEntry { + method: "payment_balance_subscribe", + kind: WireKind::Subscription { + start_id: 118, + stop_id: 119, + interrupt_id: 120, + receive_id: 121, + }, + }, + WireEntry { + method: "payment_top_up", + kind: WireKind::Request { + request_id: 122, + response_id: 123, + }, + }, + WireEntry { + method: "payment_request", + kind: WireKind::Request { + request_id: 124, + response_id: 125, + }, + }, + WireEntry { + method: "payment_status_subscribe", + kind: WireKind::Subscription { + start_id: 126, + stop_id: 127, + interrupt_id: 128, + receive_id: 129, + }, + }, + WireEntry { + method: "resource_allocation_request", + kind: WireKind::Request { + request_id: 130, + response_id: 131, + }, + }, + WireEntry { + method: "statement_store_create_proof_authorized", + kind: WireKind::Request { + request_id: 132, + response_id: 133, + }, + }, +]; diff --git a/rust/crates/truapi-codegen/tests/golden_rust_emit.rs b/rust/crates/truapi-codegen/tests/golden_rust_emit.rs new file mode 100644 index 00000000..9a059842 --- /dev/null +++ b/rust/crates/truapi-codegen/tests/golden_rust_emit.rs @@ -0,0 +1,179 @@ +//! Golden snapshot test for the Rust dispatcher emitter. +//! +//! The `ApiDefinition` model has no `Deserialize` impl (it is built by +//! the rustdoc extractor), so the "fixture" is a small JSON description +//! that this test deserializes into an `ApiDefinition` via a private +//! helper. The expected `dispatcher.rs` / `wire_table.rs` outputs live +//! under `tests/golden/`. + +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +/// Run the codegen binary directly. The binary accepts a rustdoc JSON +/// path; instead of generating that file at test time, this test boots +/// the codegen library via a small helper binary we ship alongside. +/// Easier: invoke the library's emitter via the codegen crate's +/// public modules. Since `truapi-codegen` is a `bin` crate, we use a +/// trick: include the source as a path with `#[path = "..."]`. +/// +/// Simpler approach: drive the test through the binary's CLI by +/// pre-generating a rustdoc JSON fixture. To avoid checking in a 12 MB +/// rustdoc dump, the test runs `cargo +nightly rustdoc -p truapi` +/// itself when the workspace toolchain has the nightly compiler. If +/// nightly isn't available the test prints a notice and exits. +#[test] +fn golden_dispatcher_and_wire_table() { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let workspace_root = manifest_dir + .ancestors() + .nth(3) + .expect("workspace root above rust/crates/truapi-codegen") + .to_path_buf(); + + // Generate rustdoc JSON for the truapi crate. This is what + // scripts/codegen.sh uses in production. Skip the test if nightly + // rustc isn't installed (the toolchain is required by `--output-format json`). + let rustdoc = Command::new("cargo") + .args([ + "+nightly", + "rustdoc", + "-p", + "truapi", + "--", + "-Z", + "unstable-options", + "--output-format", + "json", + ]) + .current_dir(&workspace_root) + .status(); + let rustdoc_status = match rustdoc { + Ok(status) => status, + Err(err) => { + eprintln!("skipping golden test: cargo +nightly rustdoc unavailable ({err})"); + return; + } + }; + if !rustdoc_status.success() { + eprintln!("skipping golden test: cargo +nightly rustdoc exited with {rustdoc_status}",); + return; + } + + let rustdoc_json = workspace_root.join("target/doc/truapi.json"); + assert!( + rustdoc_json.exists(), + "rustdoc JSON not found at {}", + rustdoc_json.display() + ); + + let tempdir = tempfile::tempdir().expect("tempdir"); + 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 both 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. + let golden_dir = manifest_dir.join("tests/golden"); + let cases = [ + ("wire_table.rs", "wire_table.rs"), + ("dispatcher.rs", "dispatcher.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() + ); + } + } +} + +/// 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 +/// inline unit tests might miss because they exercise smaller APIs. +#[test] +fn binary_emission_is_idempotent() { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let workspace_root = manifest_dir + .ancestors() + .nth(3) + .expect("workspace root above rust/crates/truapi-codegen") + .to_path_buf(); + + // Reuse the rustdoc JSON if the golden test already produced it. + let rustdoc_json = workspace_root.join("target/doc/truapi.json"); + if !rustdoc_json.exists() { + let rustdoc = Command::new("cargo") + .args([ + "+nightly", + "rustdoc", + "-p", + "truapi", + "--", + "-Z", + "unstable-options", + "--output-format", + "json", + ]) + .current_dir(&workspace_root) + .status(); + match rustdoc { + Ok(s) if s.success() => {} + _ => { + eprintln!("skipping idempotence test: nightly rustdoc unavailable"); + return; + } + } + } + + let run_once = || -> (String, String) { + let tmp = tempfile::tempdir().unwrap(); + let status = Command::new(env!("CARGO_BIN_EXE_truapi-codegen")) + .args([ + "--input", + rustdoc_json.to_str().unwrap(), + "--output", + tmp.path().join("ts").to_str().unwrap(), + "--rust-output", + tmp.path().join("rust").to_str().unwrap(), + ]) + .status() + .expect("run truapi-codegen"); + assert!(status.success(), "codegen run failed"); + let dispatcher = + fs::read_to_string(tmp.path().join("rust/dispatcher.rs")).expect("read dispatcher"); + let wire_table = + fs::read_to_string(tmp.path().join("rust/wire_table.rs")).expect("read wire_table"); + (dispatcher, wire_table) + }; + + let (a_disp, a_wire) = run_once(); + let (b_disp, b_wire) = run_once(); + assert_eq!(a_disp, b_disp, "dispatcher.rs differs between runs"); + assert_eq!(a_wire, b_wire, "wire_table.rs differs between runs"); +} diff --git a/rust/crates/truapi-platform/Cargo.toml b/rust/crates/truapi-platform/Cargo.toml new file mode 100644 index 00000000..81b04016 --- /dev/null +++ b/rust/crates/truapi-platform/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "truapi-platform" +version = "0.1.0" +edition.workspace = true +description = "Platform capability traits for TrUAPI host implementations" +license = "MIT" + +[dependencies] +truapi = { path = "../truapi" } +async-trait = "0.1" +futures = "0.3" +parity-scale-codec = { version = "3", features = ["derive"] } diff --git a/rust/crates/truapi-platform/README.md b/rust/crates/truapi-platform/README.md new file mode 100644 index 00000000..cd206ab5 --- /dev/null +++ b/rust/crates/truapi-platform/README.md @@ -0,0 +1,28 @@ +# truapi-platform + +Platform capability traits for TrUAPI host implementations. + +Each platform (web/WASM, iOS/UniFFI, Android/UniFFI) implements these traits to +provide native capabilities. The TrUAPI dispatcher (in `truapi-server`) calls +into these when handling API requests from the product side. + +## Traits + +- `Storage` — scoped key-value storage (`read`, `write`, `clear`). +- `Navigation` — open URLs in the system browser. +- `Notifications` — deliver push notifications. +- `Permissions` — prompt for device and remote permissions (v0.1 split callbacks). +- `Features` — probe per-chain (or other) feature support. +- `ChainProvider` / `JsonRpcConnection` — open JSON-RPC connections to chains. +- `Accounts` — product account lookup, aliasing, proofs, identity, connection status. +- `Signing` — sign payloads and raw byte/string blobs. +- `StatementStore` — subscribe, submit, and prove statements. +- `Preimage` — lookup preimage data. + +`Platform` is a blanket-implemented supertrait that combines all of the above. + +## Versioning + +Types come from `truapi::versioned::*` (V1 enum wrappers) where possible, and +fall back to `truapi::v01::*` for inner error and value types. The crate +re-exports both modules for downstream convenience. diff --git a/rust/crates/truapi-platform/src/lib.rs b/rust/crates/truapi-platform/src/lib.rs new file mode 100644 index 00000000..cd217d0b --- /dev/null +++ b/rust/crates/truapi-platform/src/lib.rs @@ -0,0 +1,276 @@ +//! Platform abstraction traits for TrUAPI host implementations. +//! +//! Each platform (web/WASM, iOS/UniFFI, Android/UniFFI) implements these traits +//! to provide native capabilities. The TrUAPI dispatcher calls into these when +//! handling API requests from the product side. +//! +//! Type aliases here map the truapi v0.1 wire types to short, intent-revealing +//! names used at platform boundaries. They are kept as aliases so call sites +//! still flow through the canonical types defined in the `truapi` crate. + +#![forbid(unsafe_code)] + +use async_trait::async_trait; +use futures::stream::BoxStream; + +use truapi::v01::{ + GenericError, HostAccountCreateProofError, HostAccountGetError, HostGetUserIdError, + HostLocalStorageReadError, HostNavigateToError, HostPushNotificationRequest, + HostSignPayloadError, RemoteStatementStoreCreateProofError, +}; +use truapi::versioned::account::{ + HostAccountConnectionStatusSubscribeItem, HostAccountCreateProofRequest, + HostAccountCreateProofResponse, HostAccountGetAliasRequest, HostAccountGetAliasResponse, + HostAccountGetRequest, HostAccountGetResponse, HostGetLegacyAccountsRequest, + HostGetLegacyAccountsResponse, HostGetUserIdRequest, HostGetUserIdResponse, +}; +use truapi::versioned::preimage::{ + RemotePreimageLookupSubscribeItem, RemotePreimageLookupSubscribeRequest, +}; +use truapi::versioned::signing::{ + HostSignPayloadRequest, HostSignPayloadResponse, HostSignRawRequest, HostSignRawResponse, +}; +use truapi::versioned::statement_store::{ + RemoteStatementStoreCreateProofRequest, RemoteStatementStoreCreateProofResponse, + RemoteStatementStoreSubmitRequest, RemoteStatementStoreSubscribeItem, + RemoteStatementStoreSubscribeRequest, +}; +use truapi::versioned::system::{HostFeatureSupportedRequest, HostFeatureSupportedResponse}; + +/// Re-export of `truapi::v01` for platform implementors. +pub use truapi::v01; +/// Re-export of `truapi::versioned` for platform implementors. +pub use truapi::versioned; + +/// Key used by the [`Storage`] trait. Strings are namespaced per-product by the +/// platform implementation. +pub type StorageKey = String; + +/// Value stored by the [`Storage`] trait. +pub type StorageValue = Vec; + +/// Error returned by [`Storage`] operations. +pub type StorageError = HostLocalStorageReadError; + +/// URL navigation error. +pub type NavigateToError = HostNavigateToError; + +/// Push-notification payload delivered by [`Notifications`]. +pub type PushNotification = HostPushNotificationRequest; + +/// SCALE-encoded chain genesis hash, used to pick a JSON-RPC endpoint. +pub type GenesisHash = Vec; + +/// Error returned by account credential lookups (`host_account_get`, +/// `host_account_get_alias`, legacy account enumeration). +pub type RequestCredentialsError = HostAccountGetError; + +/// Error returned by ring VRF proof creation. +pub type CreateProofError = HostAccountCreateProofError; + +/// Error returned by user-identity disclosure. +pub type UserIdentityError = HostGetUserIdError; + +/// Error returned by host signing. +pub type SigningError = HostSignPayloadError; + +/// Error returned by statement-store proof creation. +pub type StatementProofError = RemoteStatementStoreCreateProofError; + +/// Scoped key-value storage. The platform namespaces keys so different products +/// cannot read each other's data. +#[async_trait] +pub trait Storage: Send + Sync { + /// Read a value by key. + async fn read(&self, key: StorageKey) -> Result, StorageError>; + /// Write a value to a key. + async fn write(&self, key: StorageKey, value: StorageValue) -> Result<(), StorageError>; + /// Clear a value at a key. + async fn clear(&self, key: StorageKey) -> Result<(), StorageError>; +} + +/// Open URLs in the system browser. Input is already trimmed, categorized, +/// and (where needed) normalized by the core, the host implementation is +/// expected to do nothing more than hand the URL to the OS URL handler. +#[async_trait] +pub trait Navigation: Send + Sync { + /// Open the given URL in the system browser. + async fn navigate_to(&self, url: String) -> Result<(), NavigateToError>; +} + +/// Deliver push notifications. +#[async_trait] +pub trait Notifications: Send + Sync { + /// Push the given notification to the user. + async fn push_notification(&self, notification: PushNotification) -> Result<(), GenericError>; +} + +/// Permission prompts. The v0.1 wire protocol keeps device permissions +/// (camera, mic, NFC, ...) separate from remote permissions (domain access, +/// chain submit, ...), so the platform surface mirrors that split. +#[async_trait] +pub trait Permissions: Send + Sync { + /// Prompt the user for a device-level permission. + async fn device_permission( + &self, + request: v01::HostDevicePermissionRequest, + ) -> Result; + + /// Prompt the user for a remote (product-scoped) permission bundle. + async fn remote_permission( + &self, + request: v01::RemotePermissionRequest, + ) -> Result; +} + +/// Feature-support probing. The host answers whether it can service a given +/// capability (currently scoped to per-chain support). +#[async_trait] +pub trait Features: Send + Sync { + /// Report whether the requested feature is supported. + async fn feature_supported( + &self, + request: HostFeatureSupportedRequest, + ) -> Result; +} + +/// JSON-RPC provider factory for chain access. +/// +/// The platform provides a way to get a JSON-RPC connection for a given chain. +/// The server runtime manages the chainHead v1 state machine on top of this. +#[async_trait] +pub trait ChainProvider: Send + Sync { + /// Open a JSON-RPC connection for the chain identified by `genesis_hash`. + /// Drop the returned connection to disconnect. + async fn connect( + &self, + genesis_hash: GenesisHash, + ) -> Result, GenericError>; +} + +/// A live JSON-RPC connection to a chain. +pub trait JsonRpcConnection: Send + Sync { + /// Send a JSON-RPC request string. + fn send(&self, request: String); + + /// Stream of JSON-RPC response strings. + fn responses(&self) -> BoxStream<'static, String>; +} + +/// Account lookup, aliasing, proof generation, and connection status. +#[async_trait] +pub trait Accounts: Send + Sync { + /// Retrieve a product-scoped account. + async fn host_account_get( + &self, + request: HostAccountGetRequest, + ) -> Result; + + /// Retrieve a contextual alias for a product account. + async fn host_account_get_alias( + &self, + request: HostAccountGetAliasRequest, + ) -> Result; + + /// Generate a ring VRF proof for a product account. + async fn host_account_create_proof( + &self, + request: HostAccountCreateProofRequest, + ) -> Result; + + /// List non-product (legacy) accounts the user owns. + async fn host_get_legacy_accounts( + &self, + request: HostGetLegacyAccountsRequest, + ) -> Result; + + /// Subscribe to account connection status changes. + async fn host_account_connection_status_subscribe( + &self, + ) -> BoxStream<'static, HostAccountConnectionStatusSubscribeItem>; + + /// Fetch the user's primary identity. + async fn host_get_user_id( + &self, + request: HostGetUserIdRequest, + ) -> Result; +} + +/// Signing. Dotli's contract needs sign_payload and sign_raw, the runtime +/// leaves `host_create_transaction*` on its default "unavailable" body until a +/// host surfaces them. +#[async_trait] +pub trait Signing: Send + Sync { + /// Sign a SCALE-encoded extrinsic payload. + async fn host_sign_payload( + &self, + request: HostSignPayloadRequest, + ) -> Result; + + /// Sign a raw payload (bytes or string). + async fn host_sign_raw( + &self, + request: HostSignRawRequest, + ) -> Result; +} + +/// Statement store subscribe, submit, and proof creation. +#[async_trait] +pub trait StatementStore: Send + Sync { + /// Subscribe to statements matching the given topic filter. + async fn remote_statement_store_subscribe( + &self, + request: RemoteStatementStoreSubscribeRequest, + ) -> BoxStream<'static, RemoteStatementStoreSubscribeItem>; + + /// Submit a signed statement to the network. + async fn remote_statement_store_submit( + &self, + request: RemoteStatementStoreSubmitRequest, + ) -> Result<(), GenericError>; + + /// Create a cryptographic proof for a statement. + async fn remote_statement_store_create_proof( + &self, + request: RemoteStatementStoreCreateProofRequest, + ) -> Result; +} + +/// Preimage lookup. +#[async_trait] +pub trait Preimage: Send + Sync { + /// Subscribe to lookups for the given preimage key. + async fn remote_preimage_lookup_subscribe( + &self, + request: RemotePreimageLookupSubscribeRequest, + ) -> BoxStream<'static, RemotePreimageLookupSubscribeItem>; +} + +/// Combined platform interface. A host must provide all of these. +pub trait Platform: + Navigation + + Notifications + + Permissions + + Features + + Storage + + ChainProvider + + Accounts + + Signing + + StatementStore + + Preimage +{ +} + +impl Platform for T where + T: Navigation + + Notifications + + Permissions + + Features + + Storage + + ChainProvider + + Accounts + + Signing + + StatementStore + + Preimage +{ +} diff --git a/rust/crates/truapi-platform/tests/object_safety.rs b/rust/crates/truapi-platform/tests/object_safety.rs new file mode 100644 index 00000000..1000424b --- /dev/null +++ b/rust/crates/truapi-platform/tests/object_safety.rs @@ -0,0 +1,5 @@ +//! Compile-time check that the Platform trait composition stays object-safe / Send / Sync. + +use truapi_platform::Platform; + +fn _assert_platform_bounds() {} diff --git a/rust/crates/truapi-server/Cargo.toml b/rust/crates/truapi-server/Cargo.toml new file mode 100644 index 00000000..369027f5 --- /dev/null +++ b/rust/crates/truapi-server/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "truapi-server" +version = "0.1.0" +edition.workspace = true +description = "TrUAPI server runtime: dispatcher, frames, SCALE, streams" +license = "MIT" + +[lib] +crate-type = ["rlib", "cdylib", "staticlib"] + +[features] +default = [] +ws-bridge = ["dep:tokio", "dep:tokio-tungstenite", "dep:rand"] + +[dependencies] +truapi = { path = "../truapi" } +truapi-platform = { path = "../truapi-platform" } +async-trait = "0.1" +futures = "0.3" +parity-scale-codec = { version = "3", features = ["derive"] } +thiserror = "1" +unicode-normalization = "0.1" +url = "2" +uniffi = "0.29.4" +hex = "0.4" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1", features = ["rt-multi-thread", "net", "sync", "macros", "io-util"], optional = true } +tokio-tungstenite = { version = "0.21", default-features = false, features = ["handshake"], optional = true } +rand = { version = "0.8", optional = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys = "0.3" +wasm-bindgen = "0.2.118" +wasm-bindgen-futures = "0.4" +futures-timer = { version = "3", features = ["wasm-bindgen"] } +futures-util = "0.3" +getrandom = { version = "0.2", features = ["js"] } +pin-project = "1" +send_wrapper = { version = "0.6", features = ["futures"] } +web-time = "1" +web-sys = { version = "0.3", features = [ + "BinaryType", + "CloseEvent", + "console", + "Event", + "MessageEvent", + "WebSocket", +] } + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +tokio = { version = "1", features = ["rt-multi-thread", "net", "sync", "macros", "io-util", "time"] } +tokio-tungstenite = { version = "0.21", default-features = false, features = ["connect"] } diff --git a/rust/crates/truapi-server/README.md b/rust/crates/truapi-server/README.md new file mode 100644 index 00000000..e39bca78 --- /dev/null +++ b/rust/crates/truapi-server/README.md @@ -0,0 +1,36 @@ +# truapi-server + +*Runtime core for TrUAPI: dispatcher, protocol frames, SCALE-coded wire envelope.* + +## What this crate is for + +`truapi-server` is the runtime that turns trait implementations of the +`truapi` API into a working host. It owns: + +- the [`ProtocolMessage`] wire envelope and SCALE codec +- the [`Dispatcher`] that routes incoming frames to per-method handlers +- the subscription lifecycle (start/receive/stop/interrupt) +- the [`Transport`] trait that platform-specific IPC backends implement +- the auto-generated dispatcher/wire-table tables shipped under + [`crate::generated`] + +This crate is **Phase 4a** of the rust-core port (issue #96): only the +frame, dispatcher, subscription, transport and committed-codegen modules +are present. `core`, `runtime`, `host_logic`, chain, ws_bridge, native +and wasm bridges arrive in later waves. + +## Wire envelope + +Every frame on the wire is encoded as: + +```text +[requestId: SCALE str][discriminant: u8][payload bytes...] +``` + +The discriminant maps to a method/kind tag via the auto-generated +[`crate::generated::wire_table::WIRE_TABLE`]. Method ordering is part of +the wire protocol; only ever append. + +The payload bytes are the SCALE-encoded inner value, inlined without a +length prefix. In-memory we keep the tag as a `String` so the dispatcher +(which keys on method name) is independent of the wire numbering. diff --git a/rust/crates/truapi-server/src/core.rs b/rust/crates/truapi-server/src/core.rs new file mode 100644 index 00000000..0115ba7f --- /dev/null +++ b/rust/crates/truapi-server/src/core.rs @@ -0,0 +1,368 @@ +//! `TrUApiCore`: the entrypoint a host wraps around a `truapi::api::TrUApi` +//! implementation (direct path) or a `truapi_platform::Platform` +//! implementation (platform path). +//! +//! Direct path: `TrUApiCore::new(host)` accepts anything implementing +//! the unified [`truapi::api::TrUApi`] super-trait. Useful for unit tests +//! and bespoke hosts. +//! +//! Platform path: [`TrUApiCore::from_platform`] takes a +//! [`truapi_platform::Platform`] and wires it through +//! [`crate::runtime::PlatformRuntimeHost`] before registering with the +//! generated dispatcher. This is the path real platform shims (UniFFI, +//! wasm-bindgen, ws-bridge, ...) take. + +use std::sync::{Arc, Mutex}; + +use parity_scale_codec::{Decode, Encode}; +use truapi::api::TrUApi; +use truapi_platform::Platform; + +use crate::generated::dispatcher; +use crate::host_logic::session::SessionState; +use crate::runtime::PlatformRuntimeHost; +use crate::{Dispatcher, ProtocolMessage, Transport}; + +/// Top-level core. Owns the dispatcher and, on the platform path, the shared +/// session-state holder. +pub struct TrUApiCore { + dispatcher: Dispatcher, + /// Always present; empty for [`Self::new`] (no platform feeding it), + /// connected to a [`PlatformRuntimeHost`] for [`Self::from_platform`]. + session_state: Arc, +} + +impl TrUApiCore { + /// Build a core around a direct `TrUApi` implementation. The session + /// state holder is unused on this path (no platform pushes updates), + /// but is created anyway so the public API surface stays consistent. + pub fn new

(host: Arc

) -> Self + where + P: TrUApi + 'static, + { + let mut dispatcher = Dispatcher::new(); + dispatcher::register(&mut dispatcher, host); + Self { + dispatcher, + session_state: SessionState::new(), + } + } + + /// Build a core around a [`Platform`] implementation. Wraps the platform + /// in a [`PlatformRuntimeHost`] before registering with the dispatcher. + pub fn from_platform

(platform: Arc

) -> Self + where + P: Platform + 'static, + { + let runtime = Arc::new(PlatformRuntimeHost::new(platform)); + let session_state = runtime.session_state(); + let mut dispatcher = Dispatcher::new(); + dispatcher::register(&mut dispatcher, runtime); + Self { + dispatcher, + session_state, + } + } + + /// Handle to the shared session-state holder. Platform bridges push + /// `setActiveSession` / `clearActiveSession` notifications through this. + pub fn session_state(&self) -> Arc { + self.session_state.clone() + } + + /// Asynchronous form of [`Self::receive_from_product`]. Decodes the + /// incoming frame, runs it through the dispatcher, and returns the + /// SCALE-encoded response (if any). + pub async fn receive_from_product_async(&self, frame: &[u8]) -> Option> { + let message = ProtocolMessage::decode(&mut &*frame).ok()?; + let transport = Arc::new(ResponseTransport::default()); + self.dispatcher + .dispatch(message, transport.clone() as Arc) + .await; + transport.take().map(|response| response.encode()) + } + + /// Synchronous wrapper that blocks the current thread until the inner + /// future resolves. Convenient for embedding contexts (e.g. UniFFI) that + /// don't already drive an async runtime. + pub fn receive_from_product(&self, frame: &[u8]) -> Option> { + futures::executor::block_on(self.receive_from_product_async(frame)) + } + + /// Dispatch an already-decoded protocol message through the underlying + /// dispatcher. Bridges that own a long-lived transport (e.g. WebSocket, + /// JS callback) call this directly so subscription items flow back + /// through the bridge transport instead of the single-slot capture used + /// by [`Self::receive_from_product`]. + pub async fn dispatch(&self, message: ProtocolMessage, transport: Arc) { + self.dispatcher.dispatch(message, transport).await; + } +} + +/// Single-slot transport that captures the next response the dispatcher +/// emits. Used by [`TrUApiCore::receive_from_product`] to bridge between the +/// dispatcher's push model and the synchronous "decode in, encoded out" +/// shape exposed to embedders. +#[derive(Default)] +struct ResponseTransport { + response: Mutex>, +} + +impl ResponseTransport { + fn take(&self) -> Option { + self.response.lock().unwrap().take() + } +} + +impl Transport for ResponseTransport { + fn send(&self, message: ProtocolMessage) { + *self.response.lock().unwrap() = Some(message); + } + + fn on_message( + &self, + _handler: Box, + ) -> Box { + Box::new(|| {}) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use async_trait::async_trait; + use futures::stream::{self, BoxStream}; + use parity_scale_codec::Encode; + use truapi::v01; + use truapi::versioned::account::{ + HostAccountConnectionStatusSubscribeItem, HostAccountCreateProofRequest, + HostAccountCreateProofResponse, HostAccountGetAliasRequest, HostAccountGetAliasResponse, + HostAccountGetRequest, HostAccountGetResponse, HostGetLegacyAccountsRequest, + HostGetLegacyAccountsResponse, HostGetUserIdRequest, HostGetUserIdResponse, + }; + use truapi::versioned::preimage::{ + RemotePreimageLookupSubscribeItem, RemotePreimageLookupSubscribeRequest, + }; + use truapi::versioned::signing::{ + HostSignPayloadRequest, HostSignPayloadResponse, HostSignRawRequest, HostSignRawResponse, + }; + use truapi::versioned::statement_store::{ + RemoteStatementStoreCreateProofRequest, RemoteStatementStoreCreateProofResponse, + RemoteStatementStoreSubmitRequest, RemoteStatementStoreSubscribeItem, + RemoteStatementStoreSubscribeRequest, + }; + use truapi::versioned::system::{HostFeatureSupportedRequest, HostFeatureSupportedResponse}; + use truapi_platform::{ + Accounts as PlatformAccounts, ChainProvider, Features, GenesisHash, JsonRpcConnection, + Navigation, Notifications, Permissions, Preimage as PlatformPreimage, + Signing as PlatformSigning, StatementStore as PlatformStatementStore, Storage, + }; + + use crate::frame::{FrameKind, Payload, compose_action}; + + struct StubPlatform; + + #[async_trait] + impl Storage for StubPlatform { + async fn read( + &self, + _key: String, + ) -> Result>, v01::HostLocalStorageReadError> { + Ok(None) + } + async fn write( + &self, + _key: String, + _value: Vec, + ) -> Result<(), v01::HostLocalStorageReadError> { + Ok(()) + } + async fn clear(&self, _key: String) -> Result<(), v01::HostLocalStorageReadError> { + Ok(()) + } + } + + #[async_trait] + impl Navigation for StubPlatform { + async fn navigate_to(&self, _url: String) -> Result<(), v01::HostNavigateToError> { + Ok(()) + } + } + + #[async_trait] + impl Notifications for StubPlatform { + async fn push_notification( + &self, + _notification: v01::HostPushNotificationRequest, + ) -> Result<(), v01::GenericError> { + Ok(()) + } + } + + #[async_trait] + impl Permissions for StubPlatform { + async fn device_permission( + &self, + _request: v01::HostDevicePermissionRequest, + ) -> Result { + Ok(v01::HostDevicePermissionResponse { granted: true }) + } + async fn remote_permission( + &self, + _request: v01::RemotePermissionRequest, + ) -> Result { + Ok(v01::RemotePermissionResponse { granted: true }) + } + } + + #[async_trait] + impl Features for StubPlatform { + async fn feature_supported( + &self, + request: HostFeatureSupportedRequest, + ) -> Result { + let HostFeatureSupportedRequest::V1(_) = request; + Ok(HostFeatureSupportedResponse::V1( + v01::HostFeatureSupportedResponse { supported: true }, + )) + } + } + + struct DeadConnection; + impl JsonRpcConnection for DeadConnection { + fn send(&self, _request: String) {} + fn responses(&self) -> BoxStream<'static, String> { + Box::pin(stream::empty()) + } + } + + #[async_trait] + impl ChainProvider for StubPlatform { + async fn connect( + &self, + _genesis_hash: GenesisHash, + ) -> Result, v01::GenericError> { + Ok(Box::new(DeadConnection)) + } + } + + #[async_trait] + impl PlatformAccounts for StubPlatform { + async fn host_account_get( + &self, + _request: HostAccountGetRequest, + ) -> Result { + Err(v01::HostAccountGetError::NotConnected) + } + async fn host_account_get_alias( + &self, + _request: HostAccountGetAliasRequest, + ) -> Result { + Err(v01::HostAccountGetError::NotConnected) + } + async fn host_account_create_proof( + &self, + _request: HostAccountCreateProofRequest, + ) -> Result { + Err(v01::HostAccountCreateProofError::RingNotFound) + } + async fn host_get_legacy_accounts( + &self, + _request: HostGetLegacyAccountsRequest, + ) -> Result { + Ok(HostGetLegacyAccountsResponse::V1( + v01::HostGetLegacyAccountsResponse { accounts: vec![] }, + )) + } + async fn host_account_connection_status_subscribe( + &self, + ) -> BoxStream<'static, HostAccountConnectionStatusSubscribeItem> { + Box::pin(stream::empty()) + } + async fn host_get_user_id( + &self, + _request: HostGetUserIdRequest, + ) -> Result { + Err(v01::HostGetUserIdError::NotConnected) + } + } + + #[async_trait] + impl PlatformSigning for StubPlatform { + async fn host_sign_payload( + &self, + _request: HostSignPayloadRequest, + ) -> Result { + Err(v01::HostSignPayloadError::Rejected) + } + async fn host_sign_raw( + &self, + _request: HostSignRawRequest, + ) -> Result { + Err(v01::HostSignPayloadError::Rejected) + } + } + + #[async_trait] + impl PlatformStatementStore for StubPlatform { + async fn remote_statement_store_subscribe( + &self, + _request: RemoteStatementStoreSubscribeRequest, + ) -> BoxStream<'static, RemoteStatementStoreSubscribeItem> { + Box::pin(stream::empty()) + } + async fn remote_statement_store_submit( + &self, + _request: RemoteStatementStoreSubmitRequest, + ) -> Result<(), v01::GenericError> { + Ok(()) + } + async fn remote_statement_store_create_proof( + &self, + _request: RemoteStatementStoreCreateProofRequest, + ) -> Result< + RemoteStatementStoreCreateProofResponse, + v01::RemoteStatementStoreCreateProofError, + > { + Err(v01::RemoteStatementStoreCreateProofError::UnableToSign) + } + } + + #[async_trait] + impl PlatformPreimage for StubPlatform { + async fn remote_preimage_lookup_subscribe( + &self, + _request: RemotePreimageLookupSubscribeRequest, + ) -> BoxStream<'static, RemotePreimageLookupSubscribeItem> { + Box::pin(stream::empty()) + } + } + + #[test] + fn from_platform_dispatches_feature_supported() { + let core = TrUApiCore::from_platform(Arc::new(StubPlatform)); + let request = HostFeatureSupportedRequest::V1(v01::HostFeatureSupportedRequest::Chain { + genesis_hash: vec![0u8; 32], + }); + let frame = ProtocolMessage { + request_id: "p:1".into(), + payload: Payload { + tag: compose_action("system_feature_supported", FrameKind::Request), + value: request.encode(), + }, + }; + let encoded = frame.encode(); + let response_bytes = core + .receive_from_product(&encoded) + .expect("dispatcher should emit a response"); + let response = ProtocolMessage::decode(&mut &response_bytes[..]).expect("decode response"); + assert_eq!(response.request_id, "p:1"); + assert_eq!( + response.payload.tag, + compose_action("system_feature_supported", FrameKind::Response), + ); + // Wire payload is `Result`-shaped: + // [Ok disc=0x00][V1 variant 0x00][supported=1] + assert_eq!(response.payload.value, vec![0x00, 0x00, 0x01]); + } +} diff --git a/rust/crates/truapi-server/src/debug_log.rs b/rust/crates/truapi-server/src/debug_log.rs new file mode 100644 index 00000000..0ac94da2 --- /dev/null +++ b/rust/crates/truapi-server/src/debug_log.rs @@ -0,0 +1,47 @@ +//! Runtime-toggled debug logging. +//! +//! Enabled by the host setting a process-wide flag. Output goes to +//! `console.log` on wasm and `eprintln!` on native, prefixed with +//! `[truapi]` so it's easy to grep for. +//! +//! The macro is a no-op when disabled: format args are not evaluated, +//! so callers can `truapi_debug!("payload={:?}", expensive)` without +//! paying for the formatting on hot paths. + +use std::sync::atomic::{AtomicBool, Ordering}; + +static DEBUG_ENABLED: AtomicBool = AtomicBool::new(false); + +/// Turn the [`truapi_debug!`] macro on or off. Idempotent. +pub fn set_enabled(enabled: bool) { + DEBUG_ENABLED.store(enabled, Ordering::Relaxed); +} + +/// Whether debug logging is currently active. Cheap atomic read, safe to +/// call on hot paths so the macro can early-out before formatting. +pub fn is_enabled() -> bool { + DEBUG_ENABLED.load(Ordering::Relaxed) +} + +/// Native variant of `emit`: writes to stderr. +#[cfg(not(target_arch = "wasm32"))] +pub fn emit(line: &str) { + eprintln!("{line}"); +} + +/// Wasm variant of `emit`: routes to the browser console. +#[cfg(target_arch = "wasm32")] +pub fn emit(line: &str) { + // Stub: full wasm bridge lands in Phase 4e along with web-sys. + let _ = line; +} + +/// Emit a debug log line when [`is_enabled`] is true. +#[macro_export] +macro_rules! truapi_debug { + ($($arg:tt)*) => {{ + if $crate::debug_log::is_enabled() { + $crate::debug_log::emit(&format!("[truapi] {}", format_args!($($arg)*))); + } + }}; +} diff --git a/rust/crates/truapi-server/src/dispatcher.rs b/rust/crates/truapi-server/src/dispatcher.rs new file mode 100644 index 00000000..2d646fec --- /dev/null +++ b/rust/crates/truapi-server/src/dispatcher.rs @@ -0,0 +1,302 @@ +//! Request dispatcher. +//! +//! Routes incoming frames to the appropriate trait method based on the +//! action tag. The handler set is registered by the auto-generated +//! [`crate::generated::dispatcher::register`] function; this module +//! provides the framework that owns the registration table and the +//! routing logic. + +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::atomic::AtomicU8; + +use futures::future::LocalBoxFuture; + +use crate::frame::{FrameKind, Payload, ProtocolMessage, compose_action}; +use crate::subscription::{SubscriptionManager, SubscriptionStream}; +use crate::transport::Transport; + +/// Latest wire-codec version this server implements. Used as the default +/// negotiated version until the system `handshake` resolves; clients on +/// the same major version keep working without an explicit handshake. +pub const LATEST_PROTOCOL_VERSION: u8 = 1; + +/// A handler for a request-response method. The returned future is not +/// required to be `Send` because the truapi trait uses `async fn`, whose +/// auto-Send-ness is not guaranteed. The `request_id` is the per-frame +/// identifier; handlers thread it into the `CallContext` so trait methods +/// can correlate logs/cancellation with the originating request. +pub type RequestHandler = Arc< + dyn Fn(String, Vec) -> LocalBoxFuture<'static, Result, ProtocolMessage>> + + Send + + Sync, +>; + +/// A handler for a subscription method. +pub type SubscriptionHandler = Arc< + dyn Fn(String, Vec) -> LocalBoxFuture<'static, Result> + + Send + + Sync, +>; + +/// Routes incoming protocol messages to registered handlers. +pub struct Dispatcher { + request_handlers: HashMap, + subscription_handlers: HashMap, + subscriptions: SubscriptionManager, + /// Wire-codec version negotiated through `handshake`. The handshake + /// handler stores the requested version here (writing through the + /// shared `Arc`) so subsequent responses encode in matching wire bytes. + negotiated_version: Arc, +} + +impl Dispatcher { + /// Construct a dispatcher with a fresh negotiated-version slot + /// (defaults to [`LATEST_PROTOCOL_VERSION`]). + pub fn new() -> Self { + Self::with_negotiated_version(Arc::new(AtomicU8::new(LATEST_PROTOCOL_VERSION))) + } + + /// Construct a dispatcher that shares its negotiated-version slot + /// with another component (typically the host's handshake handler). + pub fn with_negotiated_version(negotiated_version: Arc) -> Self { + Self { + request_handlers: HashMap::new(), + subscription_handlers: HashMap::new(), + subscriptions: SubscriptionManager::new(), + negotiated_version, + } + } + + /// Clone the shared negotiated-version slot. The handshake handler + /// writes to this; per-method registration captures it for response + /// wrapping. + pub fn negotiated_version(&self) -> Arc { + self.negotiated_version.clone() + } + + /// Register a request-response handler for a method. Returns the + /// previously registered handler if any; callers (the generated + /// `dispatcher::register`) should treat `Some` as a programming error + /// since each wire method must own exactly one handler. + pub fn on_request(&mut self, method: &str, handler: F) -> Option + where + F: Fn(String, Vec) -> LocalBoxFuture<'static, Result, ProtocolMessage>> + + Send + + Sync + + 'static, + { + self.request_handlers + .insert(method.to_string(), Arc::new(handler)) + } + + /// Register a subscription handler for a method. Returns the previously + /// registered handler if any. + pub fn on_subscription(&mut self, method: &str, handler: F) -> Option + where + F: Fn( + String, + Vec, + ) -> LocalBoxFuture<'static, Result> + + Send + + Sync + + 'static, + { + self.subscription_handlers + .insert(method.to_string(), Arc::new(handler)) + } + + /// Process an incoming protocol message, sending any responses or + /// subscription frames through `transport`. + pub async fn dispatch(&self, message: ProtocolMessage, transport: Arc) { + let Some((method, kind)) = FrameKind::from_tag(&message.payload.tag) else { + return; + }; + + match kind { + FrameKind::Request => { + if let Some(handler) = self.request_handlers.get(&method) { + let request_id = message.request_id.clone(); + let result = handler(request_id, message.payload.value).await; + // On the wire, every response is `Result`-shaped: + // the handler returns `Ok(bytes)` already prefixed with a + // `0x00` discriminant for success, and `Err(ProtocolMessage)` + // whose payload is the SCALE-encoded `CallError`. The + // error path prepends `0x01` here so the wire payload is + // always `[disc][value...]`. + let payload = match result { + Ok(value) => Payload { + tag: compose_action(&method, FrameKind::Response), + value, + }, + Err(err_message) => { + let mut value = Vec::with_capacity(1 + err_message.payload.value.len()); + value.push(1u8); + value.extend_from_slice(&err_message.payload.value); + Payload { + tag: compose_action(&method, FrameKind::Response), + value, + } + } + }; + transport.send(ProtocolMessage { + request_id: message.request_id, + payload, + }); + } + } + FrameKind::Start => { + if let Some(handler) = self.subscription_handlers.get(&method) { + let request_id = message.request_id.clone(); + let stream_result = handler(request_id, message.payload.value).await; + match stream_result { + Ok(stream) => { + self.subscriptions.register( + message.request_id, + &method, + stream, + transport, + ); + } + Err(err_message) => { + transport.send(ProtocolMessage { + request_id: message.request_id, + payload: Payload { + tag: compose_action(&method, FrameKind::Interrupt), + value: err_message.payload.value, + }, + }); + } + } + } + } + FrameKind::Stop => { + self.subscriptions.handle_stop(&message.request_id); + } + // Response, Receive, Interrupt are handled by the client side. + _ => {} + } + } +} + +impl Default for Dispatcher { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use parity_scale_codec::Encode; + use std::sync::Mutex; + use truapi::CallError; + + use crate::frame::{FrameKind, Payload, compose_action}; + + #[derive(Default)] + struct RecordingTransport { + sent: Mutex>, + } + + impl RecordingTransport { + fn sent(&self) -> Vec { + self.sent.lock().unwrap().clone() + } + } + + impl Transport for RecordingTransport { + fn send(&self, message: ProtocolMessage) { + self.sent.lock().unwrap().push(message); + } + fn on_message( + &self, + _handler: Box, + ) -> Box { + Box::new(|| {}) + } + } + + fn make_frame(tag: &str, value: Vec) -> ProtocolMessage { + ProtocolMessage { + request_id: "p:1".into(), + payload: Payload { + tag: tag.to_string(), + value, + }, + } + } + + /// An unregistered method discriminant must not be answered. The + /// dispatcher reaches into its handler maps; a miss should be a silent + /// drop rather than panicking or sending a bogus response. + #[test] + fn dispatch_unknown_method_silently_drops() { + let dispatcher = Dispatcher::new(); + let transport = Arc::new(RecordingTransport::default()); + let transport_dyn: Arc = transport.clone(); + let frame = make_frame( + &compose_action("missing_method", FrameKind::Request), + Vec::new(), + ); + futures::executor::block_on(dispatcher.dispatch(frame, transport_dyn)); + assert!( + transport.sent().is_empty(), + "unknown method must not send any frames" + ); + } + + /// A handler that returns `Err(CallError::Denied)` must produce a + /// response frame whose payload begins with the `0x01` Err + /// discriminant byte (the Result wire shape). + #[test] + fn dispatch_request_handler_error_emits_response_with_err_discriminant() { + let mut dispatcher = Dispatcher::new(); + dispatcher.on_request("fake_method", |_request_id, _bytes| { + Box::pin(async move { + let err: CallError<()> = CallError::Denied; + Err(ProtocolMessage::call_error(err)) + }) + }); + let transport = Arc::new(RecordingTransport::default()); + let frame = make_frame( + &compose_action("fake_method", FrameKind::Request), + Vec::new(), + ); + futures::executor::block_on(dispatcher.dispatch(frame, transport.clone())); + let sent = transport.sent(); + assert_eq!(sent.len(), 1, "exactly one response expected"); + let payload = &sent[0].payload.value; + assert_eq!(payload.first(), Some(&1u8), "first byte must be Err disc"); + // After the Err disc comes the SCALE-encoded CallError; `Denied` is + // variant 1, so the full payload is `[0x01 disc][0x01 variant]`. + let err: CallError<()> = CallError::Denied; + let mut expected_inner = Vec::new(); + match &err { + CallError::Denied => 1u8.encode_to(&mut expected_inner), + _ => unreachable!(), + } + let mut expected = vec![1u8]; + expected.extend_from_slice(&expected_inner); + assert_eq!(payload, &expected); + } + + /// Registering two handlers under the same key must not silently + /// overwrite. The contract chosen here is "loud": `on_request` + /// returns the previous handler, so callers can detect collisions. + #[test] + fn register_request_twice_returns_previous_handler() { + let mut dispatcher = Dispatcher::new(); + let prev = dispatcher.on_request("fake_method", |_request_id, _bytes| { + Box::pin(async move { Ok(Vec::new()) }) + }); + assert!(prev.is_none(), "first registration has no predecessor"); + let prev = dispatcher.on_request("fake_method", |_request_id, _bytes| { + Box::pin(async move { Ok(Vec::new()) }) + }); + assert!( + prev.is_some(), + "second registration must return the previous handler" + ); + } +} diff --git a/rust/crates/truapi-server/src/frame.rs b/rust/crates/truapi-server/src/frame.rs new file mode 100644 index 00000000..78ea3673 --- /dev/null +++ b/rust/crates/truapi-server/src/frame.rs @@ -0,0 +1,521 @@ +//! Wire protocol frame types. +//! +//! Every message on the wire is a `ProtocolMessage` containing a `requestId` +//! and a `payload`. On the wire the envelope is: +//! +//! ```text +//! [requestId: SCALE str][discriminant: u8][payload bytes...] +//! ``` +//! +//! The discriminant maps to a method/kind tag via the auto-generated +//! [`crate::generated::wire_table::WIRE_TABLE`]. Method ordering is part of +//! the wire protocol; only ever append to the table. The payload bytes are +//! the SCALE-encoded inner value, inlined without a length prefix. +//! +//! In-memory we keep the tag as a `String` so the dispatcher (which keys on +//! method name) is unchanged. Only the codec impls below cross between +//! string-tag and discriminant. + +use parity_scale_codec::{Decode, Encode, Error as CodecError, Input, Output}; + +use truapi::CallError; + +use crate::generated::wire_table::{WIRE_TABLE, WireKind}; + +/// Top-level wire message. Encoded as `[requestId][discriminant][bytes]`; the +/// in-memory `payload.tag` carries the resolved method tag string. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProtocolMessage { + /// Per-message identifier carried by both halves of a request/response. + pub request_id: String, + /// Tagged payload describing the frame kind and SCALE bytes. + pub payload: Payload, +} + +impl ProtocolMessage { + /// Build a placeholder error frame for a decode failure. The dispatcher + /// overlays `request_id` and the response tag before sending. + pub fn decode_error(reason: String) -> Self { + let err: CallError<()> = CallError::MalformedFrame { reason }; + Self { + request_id: String::new(), + payload: Payload { + tag: String::new(), + value: encode_call_error(&err), + }, + } + } + + /// Build a placeholder error frame for a host-side failure. The dispatcher + /// overlays `request_id` and the response tag before sending. + pub fn call_error(err: CallError) -> Self { + Self { + request_id: String::new(), + payload: Payload { + tag: String::new(), + value: encode_call_error(&err), + }, + } + } +} + +impl Encode for ProtocolMessage { + fn encode_to(&self, dest: &mut T) { + self.request_id.encode_to(dest); + // Encode the discriminant. An unknown tag is a build-time bug (the + // dispatcher only emits tags it registered handlers for) but we + // poison the frame with 0xFF instead of panicking so a misconfigured + // impl can't take down the host process. The poisoned discriminant + // decodes as "unknown wire discriminant" on the peer. + let id = id_for_tag(&self.payload.tag).unwrap_or(0xFF); + id.encode_to(dest); + // Payload bytes are inlined; the receiver reads "until end of frame" + // because each transport frame is one ProtocolMessage. This matches + // the Novasama `Enum({v1: ...})` shape (variant payload encoded + // inline, no length prefix), and constrains us to slice-shaped + // `Input`s on the decode side. + dest.write(&self.payload.value); + } +} + +// Callers must hand `Decode` a slice-shaped `Input`; streaming inputs cannot +// decode this envelope because the payload has no length prefix. +impl Decode for ProtocolMessage { + fn decode(input: &mut I) -> Result { + let request_id = String::decode(input)?; + let id = u8::decode(input)?; + let tag = tag_for_id(id) + .ok_or_else(|| CodecError::from("unknown wire discriminant"))? + .to_string(); + let remaining = input + .remaining_len()? + .ok_or_else(|| CodecError::from("frame input must report remaining length"))?; + let mut value = vec![0u8; remaining]; + input.read(&mut value)?; + Ok(ProtocolMessage { + request_id, + payload: Payload { tag, value }, + }) + } +} + +/// Tagged payload. The `tag` encodes `{method}_{suffix}` where suffix is one of: +/// `request`, `response`, `start`, `receive`, `stop`, `interrupt`. +/// +/// Note: `Payload` does not derive `Encode`/`Decode` directly; the wire +/// representation lives on [`ProtocolMessage`]. `Payload` is kept as a plain +/// data type for in-memory dispatch (key on `tag`, value bytes already +/// SCALE-encoded by the call site). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Payload { + /// Method tag with frame-kind suffix. + pub tag: String, + /// SCALE-encoded inner value bytes. + pub value: Vec, +} + +/// The suffix part of an action tag, identifying the frame type. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FrameKind { + /// Client-initiated request frame. + Request, + /// Host response to a request. + Response, + /// Client-initiated subscription start. + Start, + /// Host-emitted item on a subscription. + Receive, + /// Client-initiated subscription stop. + Stop, + /// Host-emitted subscription termination. + Interrupt, +} + +impl FrameKind { + /// Return the wire suffix string for this frame kind. + pub fn suffix(&self) -> &'static str { + match self { + FrameKind::Request => "request", + FrameKind::Response => "response", + FrameKind::Start => "start", + FrameKind::Receive => "receive", + FrameKind::Stop => "stop", + FrameKind::Interrupt => "interrupt", + } + } + + /// Parse the suffix from an action tag string, returning `(method, kind)`. + pub fn from_tag(tag: &str) -> Option<(String, FrameKind)> { + for kind in [ + FrameKind::Request, + FrameKind::Response, + FrameKind::Start, + FrameKind::Receive, + FrameKind::Stop, + FrameKind::Interrupt, + ] { + let suffix = format!("_{}", kind.suffix()); + if let Some(method) = tag.strip_suffix(&suffix) { + return Some((method.to_string(), kind)); + } + } + None + } +} + +/// Build an action tag from method name and frame kind. +pub fn compose_action(method: &str, kind: FrameKind) -> String { + format!("{}_{}", method, kind.suffix()) +} + +/// Unique ID generator with a prefix. +pub struct IdFactory { + prefix: String, + counter: u64, +} + +impl IdFactory { + /// Build a factory that mints IDs of the form `{prefix}{counter}`. + pub fn new(prefix: impl Into) -> Self { + Self { + prefix: prefix.into(), + counter: 0, + } + } + + /// Return the next ID, monotonically increasing from 1. + pub fn next_id(&mut self) -> String { + self.counter += 1; + format!("{}{}", self.prefix, self.counter) + } +} + +/// Look up a discriminant by tag. Walks the generated [`WIRE_TABLE`]. +pub fn id_for_tag(tag: &str) -> Option { + let (method, kind) = FrameKind::from_tag(tag)?; + for entry in WIRE_TABLE { + if entry.method != method { + continue; + } + return match (&entry.kind, kind) { + (WireKind::Request { request_id, .. }, FrameKind::Request) => Some(*request_id), + (WireKind::Request { response_id, .. }, FrameKind::Response) => Some(*response_id), + (WireKind::Subscription { start_id, .. }, FrameKind::Start) => Some(*start_id), + (WireKind::Subscription { stop_id, .. }, FrameKind::Stop) => Some(*stop_id), + (WireKind::Subscription { interrupt_id, .. }, FrameKind::Interrupt) => { + Some(*interrupt_id) + } + (WireKind::Subscription { receive_id, .. }, FrameKind::Receive) => Some(*receive_id), + _ => None, + }; + } + None +} + +/// Look up a tag string by discriminant. Walks the generated [`WIRE_TABLE`]. +pub fn tag_for_id(id: u8) -> Option<&'static str> { + static CACHE: std::sync::OnceLock>> = + std::sync::OnceLock::new(); + let cache = CACHE.get_or_init(|| { + let max = WIRE_TABLE + .iter() + .map(|e| match &e.kind { + WireKind::Request { + request_id, + response_id, + } => (*request_id).max(*response_id), + WireKind::Subscription { + start_id, + stop_id, + interrupt_id, + receive_id, + } => (*start_id) + .max(*stop_id) + .max(*interrupt_id) + .max(*receive_id), + }) + .max() + .unwrap_or(0); + let mut table: Vec> = vec![None; usize::from(max) + 1]; + for entry in WIRE_TABLE { + match &entry.kind { + WireKind::Request { + request_id, + response_id, + } => { + table[usize::from(*request_id)] = Some((entry.method, FrameKind::Request)); + table[usize::from(*response_id)] = Some((entry.method, FrameKind::Response)); + } + WireKind::Subscription { + start_id, + stop_id, + interrupt_id, + receive_id, + } => { + table[usize::from(*start_id)] = Some((entry.method, FrameKind::Start)); + table[usize::from(*stop_id)] = Some((entry.method, FrameKind::Stop)); + table[usize::from(*interrupt_id)] = Some((entry.method, FrameKind::Interrupt)); + table[usize::from(*receive_id)] = Some((entry.method, FrameKind::Receive)); + } + } + } + table + }); + let (method, kind) = (*cache.get(usize::from(id))?)?; + // Leak the composed tag once per id so we can hand out `&'static str`. + static TAGS: std::sync::OnceLock< + std::sync::Mutex>, + > = std::sync::OnceLock::new(); + let mut map = TAGS + .get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new())) + .lock() + .ok()?; + if let Some(s) = map.get(&id) { + return Some(*s); + } + let leaked: &'static str = Box::leak(compose_action(method, kind).into_boxed_str()); + map.insert(id, leaked); + Some(leaked) +} + +/// Encode a `CallError` as SCALE bytes. `CallError` does not derive +/// `Encode` directly so the variants are emitted manually. +fn encode_call_error(err: &CallError) -> Vec { + let mut out = Vec::new(); + match err { + CallError::Domain(value) => { + 0u8.encode_to(&mut out); + value.encode_to(&mut out); + } + CallError::Denied => { + 1u8.encode_to(&mut out); + } + CallError::Unsupported => { + 2u8.encode_to(&mut out); + } + CallError::MalformedFrame { reason } => { + 3u8.encode_to(&mut out); + reason.encode_to(&mut out); + } + CallError::HostFailure { reason } => { + 4u8.encode_to(&mut out); + reason.encode_to(&mut out); + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + fn build(tag: &str, value: Vec) -> ProtocolMessage { + ProtocolMessage { + request_id: "p:1".to_string(), + payload: Payload { + tag: tag.to_string(), + value, + }, + } + } + + fn expected_wire(tag_id: u8, value: &[u8]) -> Vec { + let mut out = Vec::new(); + "p:1".to_string().encode_to(&mut out); + out.push(tag_id); + out.extend_from_slice(value); + out + } + + #[test] + fn handshake_request_encodes_with_discriminant_zero() { + // SCALE-encoded HostHandshakeRequest::V1(1u8) = [0u8 variant][1u8 codec_version] + let inner: Vec = vec![0x00, 0x01]; + let msg = build("system_handshake_request", inner.clone()); + assert_eq!(msg.encode(), expected_wire(0, &inner)); + } + + #[test] + fn get_account_request_encodes_with_discriminant_22() { + let mut inner = vec![0x00]; // V1 variant + "foo".to_string().encode_to(&mut inner); + 0u32.encode_to(&mut inner); + let msg = build("account_get_account_request", inner.clone()); + assert_eq!(msg.encode(), expected_wire(22, &inner)); + } + + #[test] + fn round_trip_preserves_tag_and_value() { + let inner: Vec = vec![0x00, 0x42, 0xab, 0xcd]; + let msg = build("local_storage_read_request", inner.clone()); + let decoded = ProtocolMessage::decode(&mut &msg.encode()[..]).expect("decode"); + assert_eq!(decoded, msg); + } + + #[test] + fn unknown_discriminant_fails_to_decode() { + let mut bytes = Vec::new(); + "p:1".to_string().encode_to(&mut bytes); + bytes.push(250); // far outside the populated range + bytes.extend_from_slice(&[0u8; 4]); + assert!(ProtocolMessage::decode(&mut &bytes[..]).is_err()); + } + + #[test] + fn subscription_phases_share_consecutive_ids() { + assert_eq!( + id_for_tag("account_connection_status_subscribe_start"), + Some(18) + ); + assert_eq!( + id_for_tag("account_connection_status_subscribe_stop"), + Some(19) + ); + assert_eq!( + id_for_tag("account_connection_status_subscribe_interrupt"), + Some(20) + ); + assert_eq!( + id_for_tag("account_connection_status_subscribe_receive"), + Some(21) + ); + } + + /// All four subscription phases round-trip through the codec, not just + /// the lookup table. Catches a regression where `Decode` mishandles a + /// frame whose payload is empty for `_stop` / `_interrupt` (no inner + /// data) but non-empty for `_start` / `_receive`. + #[test] + fn subscription_phases_round_trip_through_codec() { + let cases: &[(&str, u8, Vec)] = &[ + ( + "account_connection_status_subscribe_start", + 18, + vec![0x00, 0xaa], + ), + ("account_connection_status_subscribe_stop", 19, Vec::new()), + ( + "account_connection_status_subscribe_interrupt", + 20, + Vec::new(), + ), + ( + "account_connection_status_subscribe_receive", + 21, + vec![0x01, 0x02, 0x03, 0x04], + ), + ]; + for (tag, id, value) in cases { + let msg = build(tag, value.clone()); + let bytes = msg.encode(); + assert_eq!( + bytes, + expected_wire(*id, value), + "encode mismatch for {tag}" + ); + let decoded = ProtocolMessage::decode(&mut &bytes[..]).expect("decode"); + assert_eq!(decoded, msg, "round-trip mismatch for {tag}"); + } + } + + /// Genuine zero-byte payload (e.g. unit-typed response). `Decode` must + /// handle `remaining_len == 0` without erroring or reading past EOF. + #[test] + fn empty_payload_round_trips() { + let msg = build("local_storage_clear_response", Vec::new()); + let bytes = msg.encode(); + // [SCALE compact-len 0x0c][p][:][1][u8 17] = 4 + 1 = 5 bytes total + assert_eq!(bytes.len(), 5); + let decoded = ProtocolMessage::decode(&mut &bytes[..]).expect("decode"); + assert_eq!(decoded, msg); + } + + /// Compact-len mode 1 kicks in for strings with length 64..=16383. Make + /// sure the codec handles a long requestId without truncation. + #[test] + fn long_request_id_round_trips() { + let long_id: String = "x".repeat(200); + let msg = ProtocolMessage { + request_id: long_id, + payload: Payload { + tag: "account_get_account_request".to_string(), + value: vec![0x00, 0xab, 0xcd], + }, + }; + let decoded = ProtocolMessage::decode(&mut &msg.encode()[..]).expect("decode"); + assert_eq!(decoded, msg); + } + + /// Truncated frames must surface a `CodecError`, not panic. + #[test] + fn truncated_frames_error_cleanly() { + // Empty buffer. + assert!(ProtocolMessage::decode(&mut &[][..]).is_err()); + // Just the requestId, no discriminant byte. + let mut only_request_id = Vec::new(); + "p:1".to_string().encode_to(&mut only_request_id); + assert!(ProtocolMessage::decode(&mut &only_request_id[..]).is_err()); + // RequestId header claims length=200 but the buffer is far shorter. + let truncated_str_header = [200u8 << 2, 0x61, 0x62, 0x63]; + assert!(ProtocolMessage::decode(&mut &truncated_str_header[..]).is_err()); + } + + /// Empty requestId (zero-length string) is a valid SCALE-encoded `str` + /// (compact-len 0, no body). The codec must round-trip it without + /// confusing length-0 with EOF. + #[test] + fn empty_request_id_round_trips() { + let msg = ProtocolMessage { + request_id: String::new(), + payload: Payload { + tag: "account_get_account_request".to_string(), + value: vec![0x00, 0x01, 0x02], + }, + }; + let bytes = msg.encode(); + // [SCALE compact-len 0 = 0x00][discriminant][payload] + assert_eq!(bytes[0], 0x00); + let decoded = ProtocolMessage::decode(&mut &bytes[..]).expect("decode"); + assert_eq!(decoded, msg); + } + + /// Unicode characters round-trip through SCALE string encoding. + #[test] + fn unicode_request_id_round_trips() { + let msg = ProtocolMessage { + request_id: "héllo-世界-🦀".to_string(), + payload: Payload { + tag: "account_get_account_request".to_string(), + value: vec![0x00, 0x01], + }, + }; + let decoded = ProtocolMessage::decode(&mut &msg.encode()[..]).expect("decode"); + assert_eq!(decoded, msg); + } + + /// Large payload (>64KiB) round-trips. Catches buffer-size assumptions + /// in the inline-payload read path. + #[test] + fn large_payload_round_trips() { + let big = vec![0xa5u8; 100 * 1024]; + let msg = build("account_get_account_request", big); + let decoded = ProtocolMessage::decode(&mut &msg.encode()[..]).expect("decode"); + assert_eq!(decoded, msg); + } + + /// Discriminant 0xFF is the documented poison/escape-hatch slot. It is + /// not registered as a real tag, so encoding a frame with an unknown + /// tag falls back to 0xFF, and the decoder must reject 0xFF. + #[test] + fn poison_slot_0xff_is_unmapped_and_decode_rejects() { + assert!(tag_for_id(0xFF).is_none()); + let mut bytes = Vec::new(); + "p:1".to_string().encode_to(&mut bytes); + bytes.push(0xFF); + bytes.extend_from_slice(&[0u8; 4]); + assert!( + ProtocolMessage::decode(&mut &bytes[..]).is_err(), + "decoding the 0xFF poison slot must fail", + ); + } +} diff --git a/rust/crates/truapi-server/src/generated/dispatcher.rs b/rust/crates/truapi-server/src/generated/dispatcher.rs new file mode 100644 index 00000000..5660da55 --- /dev/null +++ b/rust/crates/truapi-server/src/generated/dispatcher.rs @@ -0,0 +1,1412 @@ +//! Wire dispatcher for the unified `TrUApi` trait. +//! +//! Auto-generated by truapi-codegen. Do not edit. + +use std::sync::Arc; + +use parity_scale_codec::{Decode, Encode}; + +use truapi::CallContext; +use truapi::api::{ + Account, Chain, Chat, Entropy, JsonRpc, LocalStorage, Payment, Permissions, Preimage, + ResourceAllocation, Signing, StatementStore, System, Theme, +}; +use truapi::versioned; + +use crate::dispatcher::Dispatcher; +use crate::frame::ProtocolMessage; +use crate::subscription::subscription_stream; + +/// Register every TrUAPI method with the dispatcher. +pub fn register

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Account + + Chain + + Chat + + Entropy + + JsonRpc + + LocalStorage + + Payment + + Permissions + + Preimage + + ResourceAllocation + + Signing + + StatementStore + + System + + Theme + + Send + + Sync + + 'static, +{ + register_account(dispatcher, host.clone()); + register_chain(dispatcher, host.clone()); + register_chat(dispatcher, host.clone()); + register_entropy(dispatcher, host.clone()); + register_jsonrpc(dispatcher, host.clone()); + register_local_storage(dispatcher, host.clone()); + register_payment(dispatcher, host.clone()); + register_permissions(dispatcher, host.clone()); + register_preimage(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); +} + +fn register_account

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Account + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription( + "account_connection_status_subscribe", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let _ = bytes; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.connection_status_subscribe(&cx).await; + Ok(subscription_stream::< + versioned::account::HostAccountConnectionStatusSubscribeItem, + _, + >(stream)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "account_get_account", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostAccountGetRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostAccountGetResponse = + match host.get_account(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "account_get_account_alias", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostAccountGetAliasRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostAccountGetAliasResponse = + match host.get_account_alias(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "account_create_account_proof", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostAccountCreateProofRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostAccountCreateProofResponse = + match host.create_account_proof(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "account_get_legacy_accounts", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostGetLegacyAccountsRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostGetLegacyAccountsResponse = + match host.get_legacy_accounts(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "account_get_user_id", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostGetUserIdRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostGetUserIdResponse = + match host.get_user_id(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host; + dispatcher.on_request( + "account_request_login", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostRequestLoginRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostRequestLoginResponse = + match host.request_login(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } +} + +fn register_chain

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Chain + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription( + "chain_follow_head_subscribe", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadFollowRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.follow_head_subscribe(&cx, request).await; + Ok(subscription_stream::< + versioned::chain::RemoteChainHeadFollowItem, + _, + >(stream)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "chain_get_head_header", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadHeaderRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadHeaderResponse = + match host.get_head_header(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "chain_get_head_body", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadBodyRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadBodyResponse = + match host.get_head_body(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "chain_get_head_storage", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadStorageRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadStorageResponse = + match host.get_head_storage(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "chain_call_head", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadCallRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadCallResponse = + match host.call_head(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "chain_unpin_head", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadUnpinRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadUnpinResponse = + match host.unpin_head(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "chain_continue_head", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadContinueRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadContinueResponse = + match host.continue_head(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "chain_stop_head_operation", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadStopOperationRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadStopOperationResponse = + match host.stop_head_operation(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "chain_get_spec_genesis_hash", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainSpecGenesisHashRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainSpecGenesisHashResponse = + match host.get_spec_genesis_hash(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "chain_get_spec_chain_name", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainSpecChainNameRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainSpecChainNameResponse = + match host.get_spec_chain_name(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "chain_get_spec_properties", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainSpecPropertiesRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainSpecPropertiesResponse = + match host.get_spec_properties(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "chain_broadcast_transaction", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainTransactionBroadcastRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainTransactionBroadcastResponse = + match host.broadcast_transaction(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host; + dispatcher.on_request( + "chain_stop_transaction", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainTransactionStopRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainTransactionStopResponse = + match host.stop_transaction(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } +} + +fn register_chat

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Chat + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request( + "chat_create_room", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chat::HostChatCreateRoomRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chat::HostChatCreateRoomResponse = + match host.create_room(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "chat_register_bot", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chat::HostChatRegisterBotRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chat::HostChatRegisterBotResponse = + match host.register_bot(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_subscription( + "chat_list_subscribe", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let _ = bytes; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.list_subscribe(&cx).await; + Ok(subscription_stream::< + versioned::chat::HostChatListSubscribeItem, + _, + >(stream)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "chat_post_message", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chat::HostChatPostMessageRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chat::HostChatPostMessageResponse = + match host.post_message(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_subscription( + "chat_action_subscribe", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let _ = bytes; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.action_subscribe(&cx).await; + Ok(subscription_stream::< + versioned::chat::HostChatActionSubscribeItem, + _, + >(stream)) + }) + }, + ); + } + { + let host = host; + dispatcher.on_subscription( + "chat_custom_message_render_subscribe", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chat::ProductChatCustomMessageRenderSubscribeRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.custom_message_render_subscribe(&cx, request).await; + Ok(subscription_stream::< + versioned::chat::ProductChatCustomMessageRenderSubscribeItem, + _, + >(stream)) + }) + }, + ); + } +} + +fn register_entropy

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Entropy + Send + Sync + 'static, +{ + { + let host = host; + dispatcher.on_request( + "entropy_derive", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::entropy::HostDeriveEntropyRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::entropy::HostDeriveEntropyResponse = + match host.derive(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } +} + +fn register_jsonrpc

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: JsonRpc + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request( + "json_rpc_send_message", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::jsonrpc::HostJsonrpcMessageSendRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::jsonrpc::HostJsonrpcMessageSendResponse = + match host.send_message(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host; + dispatcher.on_subscription( + "json_rpc_subscribe_messages", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::jsonrpc::HostJsonrpcMessageSubscribeRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.subscribe_messages(&cx, request).await; + Ok(subscription_stream::< + versioned::jsonrpc::HostJsonrpcMessageSubscribeItem, + _, + >(stream)) + }) + }, + ); + } +} + +fn register_local_storage

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: LocalStorage + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request( + "local_storage_read", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::local_storage::HostLocalStorageReadRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::local_storage::HostLocalStorageReadResponse = + match host.read(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "local_storage_write", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::local_storage::HostLocalStorageWriteRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::local_storage::HostLocalStorageWriteResponse = + match host.write(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host; + dispatcher.on_request( + "local_storage_clear", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::local_storage::HostLocalStorageClearRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::local_storage::HostLocalStorageClearResponse = + match host.clear(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } +} + +fn register_payment

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Payment + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription( + "payment_balance_subscribe", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::payment::HostPaymentBalanceSubscribeRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.balance_subscribe(&cx, request).await { + Ok(sub) => sub, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + Ok(subscription_stream::< + versioned::payment::HostPaymentBalanceSubscribeItem, + _, + >(stream)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "payment_request", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::payment::HostPaymentRequestRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::payment::HostPaymentRequestResponse = + match host.request(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_subscription( + "payment_status_subscribe", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::payment::HostPaymentStatusSubscribeRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.status_subscribe(&cx, request).await { + Ok(sub) => sub, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + Ok(subscription_stream::< + versioned::payment::HostPaymentStatusSubscribeItem, + _, + >(stream)) + }) + }, + ); + } + { + let host = host; + dispatcher.on_request( + "payment_top_up", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::payment::HostPaymentTopUpRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::payment::HostPaymentTopUpResponse = + match host.top_up(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } +} + +fn register_permissions

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Permissions + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request( + "permissions_request_device_permission", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::permissions::HostDevicePermissionRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::permissions::HostDevicePermissionResponse = + match host.request_device_permission(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host; + dispatcher.on_request( + "permissions_request_remote_permission", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::permissions::RemotePermissionRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::permissions::RemotePermissionResponse = + match host.request_remote_permission(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } +} + +fn register_preimage

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Preimage + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription( + "preimage_lookup_subscribe", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::preimage::RemotePreimageLookupSubscribeRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.lookup_subscribe(&cx, request).await; + Ok(subscription_stream::< + versioned::preimage::RemotePreimageLookupSubscribeItem, + _, + >(stream)) + }) + }, + ); + } + { + let host = host; + dispatcher.on_request( + "preimage_submit", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::preimage::RemotePreimageSubmitRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::preimage::RemotePreimageSubmitResponse = + match host.submit(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } +} + +fn register_resource_allocation

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: ResourceAllocation + Send + Sync + 'static, +{ + { + let host = host; + dispatcher.on_request("resource_allocation_request", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::resource_allocation::HostRequestResourceAllocationRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::resource_allocation::HostRequestResourceAllocationResponse = match host.request(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } +} + +fn register_signing

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Signing + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request( + "signing_create_transaction", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostCreateTransactionRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostCreateTransactionResponse = + match host.create_transaction(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request("signing_create_transaction_with_legacy_account", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostCreateTransactionWithLegacyAccountRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostCreateTransactionWithLegacyAccountResponse = match host.create_transaction_with_legacy_account(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request( + "signing_sign_raw_with_legacy_account", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostSignRawWithLegacyAccountRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostSignRawWithLegacyAccountResponse = + match host.sign_raw_with_legacy_account(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "signing_sign_payload_with_legacy_account", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostSignPayloadWithLegacyAccountRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostSignPayloadWithLegacyAccountResponse = + match host.sign_payload_with_legacy_account(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "signing_sign_raw", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostSignRawRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostSignRawResponse = + match host.sign_raw(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host; + dispatcher.on_request( + "signing_sign_payload", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostSignPayloadRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostSignPayloadResponse = + match host.sign_payload(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } +} + +fn register_statement_store

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: StatementStore + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription( + "statement_store_subscribe", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::statement_store::RemoteStatementStoreSubscribeRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.subscribe(&cx, request).await; + Ok(subscription_stream::< + versioned::statement_store::RemoteStatementStoreSubscribeItem, + _, + >(stream)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request("statement_store_create_proof", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::statement_store::RemoteStatementStoreCreateProofRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::statement_store::RemoteStatementStoreCreateProofResponse = match host.create_proof(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request("statement_store_create_proof_authorized", move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedRequest = + Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedResponse = match host.create_proof_authorized(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }); + } + { + let host = host; + dispatcher.on_request( + "statement_store_submit", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::statement_store::RemoteStatementStoreSubmitRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + match host.submit(&cx, request).await { + Ok(()) => Ok(vec![0u8]), + Err(err) => Err(ProtocolMessage::call_error(err)), + } + }) + }, + ); + } +} + +fn register_system

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: System + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request( + "system_handshake", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::system::HostHandshakeRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::system::HostHandshakeResponse = + match host.handshake(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "system_feature_supported", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::system::HostFeatureSupportedRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::system::HostFeatureSupportedResponse = + match host.feature_supported(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + "system_push_notification", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::system::HostPushNotificationRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::system::HostPushNotificationResponse = + match host.push_notification(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } + { + let host = host; + dispatcher.on_request( + "system_navigate_to", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::system::HostNavigateToRequest = + Decode::decode(&mut &bytes[..]) + .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::system::HostNavigateToResponse = + match host.navigate_to(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(ProtocolMessage::call_error(err)), + }; + let mut buf = Vec::with_capacity(1 + response.size_hint()); + buf.push(0u8); + response.encode_to(&mut buf); + Ok(buf) + }) + }, + ); + } +} + +fn register_theme

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Theme + Send + Sync + 'static, +{ + { + let host = host; + dispatcher.on_subscription( + "theme_subscribe", + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let _ = bytes; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.subscribe(&cx).await; + Ok(subscription_stream::< + versioned::theme::HostThemeSubscribeItem, + _, + >(stream)) + }) + }, + ); + } +} diff --git a/rust/crates/truapi-server/src/generated/mod.rs b/rust/crates/truapi-server/src/generated/mod.rs new file mode 100644 index 00000000..770a015d --- /dev/null +++ b/rust/crates/truapi-server/src/generated/mod.rs @@ -0,0 +1,4 @@ +//! Generated by truapi-codegen. Do not edit. + +pub mod dispatcher; +pub mod wire_table; diff --git a/rust/crates/truapi-server/src/generated/wire_table.rs b/rust/crates/truapi-server/src/generated/wire_table.rs new file mode 100644 index 00000000..d524e2bf --- /dev/null +++ b/rust/crates/truapi-server/src/generated/wire_table.rs @@ -0,0 +1,590 @@ +//! Wire-protocol discriminant table. +//! +//! Auto-generated by truapi-codegen. Do not edit. +//! +//! Each method reserves either two ids (request/response) or four +//! (start/stop/interrupt/receive). The table is sorted by request/start id. + +// entry id=0 tag="system_handshake_request" +// entry id=1 tag="system_handshake_response" +// entry id=2 tag="system_feature_supported_request" +// entry id=3 tag="system_feature_supported_response" +// entry id=4 tag="system_push_notification_request" +// entry id=5 tag="system_push_notification_response" +// entry id=6 tag="system_navigate_to_request" +// entry id=7 tag="system_navigate_to_response" +// entry id=8 tag="permissions_request_device_permission_request" +// entry id=9 tag="permissions_request_device_permission_response" +// entry id=10 tag="permissions_request_remote_permission_request" +// entry id=11 tag="permissions_request_remote_permission_response" +// entry id=12 tag="local_storage_read_request" +// entry id=13 tag="local_storage_read_response" +// entry id=14 tag="local_storage_write_request" +// entry id=15 tag="local_storage_write_response" +// entry id=16 tag="local_storage_clear_request" +// entry id=17 tag="local_storage_clear_response" +// entry id=18 tag="account_connection_status_subscribe_start" +// entry id=19 tag="account_connection_status_subscribe_stop" +// entry id=20 tag="account_connection_status_subscribe_interrupt" +// entry id=21 tag="account_connection_status_subscribe_receive" +// entry id=22 tag="account_get_account_request" +// entry id=23 tag="account_get_account_response" +// entry id=24 tag="account_get_account_alias_request" +// entry id=25 tag="account_get_account_alias_response" +// entry id=26 tag="account_create_account_proof_request" +// entry id=27 tag="account_create_account_proof_response" +// entry id=28 tag="account_get_legacy_accounts_request" +// entry id=29 tag="account_get_legacy_accounts_response" +// entry id=30 tag="signing_create_transaction_request" +// entry id=31 tag="signing_create_transaction_response" +// entry id=32 tag="signing_create_transaction_with_legacy_account_request" +// entry id=33 tag="signing_create_transaction_with_legacy_account_response" +// entry id=34 tag="signing_sign_raw_with_legacy_account_request" +// entry id=35 tag="signing_sign_raw_with_legacy_account_response" +// entry id=36 tag="signing_sign_payload_with_legacy_account_request" +// entry id=37 tag="signing_sign_payload_with_legacy_account_response" +// entry id=38 tag="chat_create_room_request" +// entry id=39 tag="chat_create_room_response" +// entry id=40 tag="chat_register_bot_request" +// entry id=41 tag="chat_register_bot_response" +// entry id=42 tag="chat_list_subscribe_start" +// entry id=43 tag="chat_list_subscribe_stop" +// entry id=44 tag="chat_list_subscribe_interrupt" +// entry id=45 tag="chat_list_subscribe_receive" +// entry id=46 tag="chat_post_message_request" +// entry id=47 tag="chat_post_message_response" +// entry id=48 tag="chat_action_subscribe_start" +// entry id=49 tag="chat_action_subscribe_stop" +// entry id=50 tag="chat_action_subscribe_interrupt" +// entry id=51 tag="chat_action_subscribe_receive" +// entry id=52 tag="chat_custom_message_render_subscribe_start" +// entry id=53 tag="chat_custom_message_render_subscribe_stop" +// entry id=54 tag="chat_custom_message_render_subscribe_interrupt" +// entry id=55 tag="chat_custom_message_render_subscribe_receive" +// entry id=56 tag="statement_store_subscribe_start" +// entry id=57 tag="statement_store_subscribe_stop" +// entry id=58 tag="statement_store_subscribe_interrupt" +// entry id=59 tag="statement_store_subscribe_receive" +// entry id=60 tag="statement_store_create_proof_request" +// entry id=61 tag="statement_store_create_proof_response" +// entry id=62 tag="statement_store_submit_request" +// entry id=63 tag="statement_store_submit_response" +// entry id=64 tag="preimage_lookup_subscribe_start" +// entry id=65 tag="preimage_lookup_subscribe_stop" +// entry id=66 tag="preimage_lookup_subscribe_interrupt" +// entry id=67 tag="preimage_lookup_subscribe_receive" +// entry id=68 tag="preimage_submit_request" +// entry id=69 tag="preimage_submit_response" +// entry id=70 tag="json_rpc_send_message_request" +// entry id=71 tag="json_rpc_send_message_response" +// entry id=72 tag="json_rpc_subscribe_messages_start" +// entry id=73 tag="json_rpc_subscribe_messages_stop" +// entry id=74 tag="json_rpc_subscribe_messages_interrupt" +// entry id=75 tag="json_rpc_subscribe_messages_receive" +// entry id=76 tag="chain_follow_head_subscribe_start" +// entry id=77 tag="chain_follow_head_subscribe_stop" +// entry id=78 tag="chain_follow_head_subscribe_interrupt" +// entry id=79 tag="chain_follow_head_subscribe_receive" +// entry id=80 tag="chain_get_head_header_request" +// entry id=81 tag="chain_get_head_header_response" +// entry id=82 tag="chain_get_head_body_request" +// entry id=83 tag="chain_get_head_body_response" +// entry id=84 tag="chain_get_head_storage_request" +// entry id=85 tag="chain_get_head_storage_response" +// entry id=86 tag="chain_call_head_request" +// entry id=87 tag="chain_call_head_response" +// entry id=88 tag="chain_unpin_head_request" +// entry id=89 tag="chain_unpin_head_response" +// entry id=90 tag="chain_continue_head_request" +// entry id=91 tag="chain_continue_head_response" +// entry id=92 tag="chain_stop_head_operation_request" +// entry id=93 tag="chain_stop_head_operation_response" +// entry id=94 tag="chain_get_spec_genesis_hash_request" +// entry id=95 tag="chain_get_spec_genesis_hash_response" +// entry id=96 tag="chain_get_spec_chain_name_request" +// entry id=97 tag="chain_get_spec_chain_name_response" +// entry id=98 tag="chain_get_spec_properties_request" +// entry id=99 tag="chain_get_spec_properties_response" +// entry id=100 tag="chain_broadcast_transaction_request" +// entry id=101 tag="chain_broadcast_transaction_response" +// entry id=102 tag="chain_stop_transaction_request" +// entry id=103 tag="chain_stop_transaction_response" +// entry id=104 tag="theme_subscribe_start" +// entry id=105 tag="theme_subscribe_stop" +// entry id=106 tag="theme_subscribe_interrupt" +// entry id=107 tag="theme_subscribe_receive" +// entry id=108 tag="entropy_derive_request" +// entry id=109 tag="entropy_derive_response" +// entry id=110 tag="account_get_user_id_request" +// entry id=111 tag="account_get_user_id_response" +// entry id=112 tag="account_request_login_request" +// entry id=113 tag="account_request_login_response" +// entry id=114 tag="signing_sign_raw_request" +// entry id=115 tag="signing_sign_raw_response" +// entry id=116 tag="signing_sign_payload_request" +// entry id=117 tag="signing_sign_payload_response" +// entry id=118 tag="payment_balance_subscribe_start" +// entry id=119 tag="payment_balance_subscribe_stop" +// entry id=120 tag="payment_balance_subscribe_interrupt" +// entry id=121 tag="payment_balance_subscribe_receive" +// entry id=122 tag="payment_top_up_request" +// entry id=123 tag="payment_top_up_response" +// entry id=124 tag="payment_request_request" +// entry id=125 tag="payment_request_response" +// entry id=126 tag="payment_status_subscribe_start" +// entry id=127 tag="payment_status_subscribe_stop" +// entry id=128 tag="payment_status_subscribe_interrupt" +// entry id=129 tag="payment_status_subscribe_receive" +// entry id=130 tag="resource_allocation_request_request" +// entry id=131 tag="resource_allocation_request_response" +// entry id=132 tag="statement_store_create_proof_authorized_request" +// entry id=133 tag="statement_store_create_proof_authorized_response" + +/// 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 pair or subscription quartet. +pub enum WireKind { + /// Request/response method. + Request { + /// Discriminant for the request frame. + request_id: u8, + /// Discriminant for the response frame. + response_id: u8, + }, + /// Subscription method. + Subscription { + /// Discriminant for the start frame. + start_id: u8, + /// Discriminant for the stop frame. + stop_id: u8, + /// Discriminant for the interrupt frame (server-initiated termination). + interrupt_id: u8, + /// Discriminant for each receive frame (a streamed item). + receive_id: u8, + }, +} + +/// The full wire table. Ordering is part of the wire protocol; +/// only ever append. Removed methods leave their slot empty. +pub const WIRE_TABLE: &[WireEntry] = &[ + WireEntry { + method: "system_handshake", + kind: WireKind::Request { + request_id: 0, + response_id: 1, + }, + }, + WireEntry { + method: "system_feature_supported", + kind: WireKind::Request { + request_id: 2, + response_id: 3, + }, + }, + WireEntry { + method: "system_push_notification", + kind: WireKind::Request { + request_id: 4, + response_id: 5, + }, + }, + WireEntry { + method: "system_navigate_to", + kind: WireKind::Request { + request_id: 6, + response_id: 7, + }, + }, + WireEntry { + method: "permissions_request_device_permission", + kind: WireKind::Request { + request_id: 8, + response_id: 9, + }, + }, + WireEntry { + method: "permissions_request_remote_permission", + kind: WireKind::Request { + request_id: 10, + response_id: 11, + }, + }, + WireEntry { + method: "local_storage_read", + kind: WireKind::Request { + request_id: 12, + response_id: 13, + }, + }, + WireEntry { + method: "local_storage_write", + kind: WireKind::Request { + request_id: 14, + response_id: 15, + }, + }, + WireEntry { + method: "local_storage_clear", + kind: WireKind::Request { + request_id: 16, + response_id: 17, + }, + }, + WireEntry { + method: "account_connection_status_subscribe", + kind: WireKind::Subscription { + start_id: 18, + stop_id: 19, + interrupt_id: 20, + receive_id: 21, + }, + }, + WireEntry { + method: "account_get_account", + kind: WireKind::Request { + request_id: 22, + response_id: 23, + }, + }, + WireEntry { + method: "account_get_account_alias", + kind: WireKind::Request { + request_id: 24, + response_id: 25, + }, + }, + WireEntry { + method: "account_create_account_proof", + kind: WireKind::Request { + request_id: 26, + response_id: 27, + }, + }, + WireEntry { + method: "account_get_legacy_accounts", + kind: WireKind::Request { + request_id: 28, + response_id: 29, + }, + }, + WireEntry { + method: "signing_create_transaction", + kind: WireKind::Request { + request_id: 30, + response_id: 31, + }, + }, + WireEntry { + method: "signing_create_transaction_with_legacy_account", + kind: WireKind::Request { + request_id: 32, + response_id: 33, + }, + }, + WireEntry { + method: "signing_sign_raw_with_legacy_account", + kind: WireKind::Request { + request_id: 34, + response_id: 35, + }, + }, + WireEntry { + method: "signing_sign_payload_with_legacy_account", + kind: WireKind::Request { + request_id: 36, + response_id: 37, + }, + }, + WireEntry { + method: "chat_create_room", + kind: WireKind::Request { + request_id: 38, + response_id: 39, + }, + }, + WireEntry { + method: "chat_register_bot", + kind: WireKind::Request { + request_id: 40, + response_id: 41, + }, + }, + WireEntry { + method: "chat_list_subscribe", + kind: WireKind::Subscription { + start_id: 42, + stop_id: 43, + interrupt_id: 44, + receive_id: 45, + }, + }, + WireEntry { + method: "chat_post_message", + kind: WireKind::Request { + request_id: 46, + response_id: 47, + }, + }, + WireEntry { + method: "chat_action_subscribe", + kind: WireKind::Subscription { + start_id: 48, + stop_id: 49, + interrupt_id: 50, + receive_id: 51, + }, + }, + WireEntry { + method: "chat_custom_message_render_subscribe", + kind: WireKind::Subscription { + start_id: 52, + stop_id: 53, + interrupt_id: 54, + receive_id: 55, + }, + }, + WireEntry { + method: "statement_store_subscribe", + kind: WireKind::Subscription { + start_id: 56, + stop_id: 57, + interrupt_id: 58, + receive_id: 59, + }, + }, + WireEntry { + method: "statement_store_create_proof", + kind: WireKind::Request { + request_id: 60, + response_id: 61, + }, + }, + WireEntry { + method: "statement_store_submit", + kind: WireKind::Request { + request_id: 62, + response_id: 63, + }, + }, + WireEntry { + method: "preimage_lookup_subscribe", + kind: WireKind::Subscription { + start_id: 64, + stop_id: 65, + interrupt_id: 66, + receive_id: 67, + }, + }, + WireEntry { + method: "preimage_submit", + kind: WireKind::Request { + request_id: 68, + response_id: 69, + }, + }, + WireEntry { + method: "json_rpc_send_message", + kind: WireKind::Request { + request_id: 70, + response_id: 71, + }, + }, + WireEntry { + method: "json_rpc_subscribe_messages", + kind: WireKind::Subscription { + start_id: 72, + stop_id: 73, + interrupt_id: 74, + receive_id: 75, + }, + }, + WireEntry { + method: "chain_follow_head_subscribe", + kind: WireKind::Subscription { + start_id: 76, + stop_id: 77, + interrupt_id: 78, + receive_id: 79, + }, + }, + WireEntry { + method: "chain_get_head_header", + kind: WireKind::Request { + request_id: 80, + response_id: 81, + }, + }, + WireEntry { + method: "chain_get_head_body", + kind: WireKind::Request { + request_id: 82, + response_id: 83, + }, + }, + WireEntry { + method: "chain_get_head_storage", + kind: WireKind::Request { + request_id: 84, + response_id: 85, + }, + }, + WireEntry { + method: "chain_call_head", + kind: WireKind::Request { + request_id: 86, + response_id: 87, + }, + }, + WireEntry { + method: "chain_unpin_head", + kind: WireKind::Request { + request_id: 88, + response_id: 89, + }, + }, + WireEntry { + method: "chain_continue_head", + kind: WireKind::Request { + request_id: 90, + response_id: 91, + }, + }, + WireEntry { + method: "chain_stop_head_operation", + kind: WireKind::Request { + request_id: 92, + response_id: 93, + }, + }, + WireEntry { + method: "chain_get_spec_genesis_hash", + kind: WireKind::Request { + request_id: 94, + response_id: 95, + }, + }, + WireEntry { + method: "chain_get_spec_chain_name", + kind: WireKind::Request { + request_id: 96, + response_id: 97, + }, + }, + WireEntry { + method: "chain_get_spec_properties", + kind: WireKind::Request { + request_id: 98, + response_id: 99, + }, + }, + WireEntry { + method: "chain_broadcast_transaction", + kind: WireKind::Request { + request_id: 100, + response_id: 101, + }, + }, + WireEntry { + method: "chain_stop_transaction", + kind: WireKind::Request { + request_id: 102, + response_id: 103, + }, + }, + WireEntry { + method: "theme_subscribe", + kind: WireKind::Subscription { + start_id: 104, + stop_id: 105, + interrupt_id: 106, + receive_id: 107, + }, + }, + WireEntry { + method: "entropy_derive", + kind: WireKind::Request { + request_id: 108, + response_id: 109, + }, + }, + WireEntry { + method: "account_get_user_id", + kind: WireKind::Request { + request_id: 110, + response_id: 111, + }, + }, + WireEntry { + method: "account_request_login", + kind: WireKind::Request { + request_id: 112, + response_id: 113, + }, + }, + WireEntry { + method: "signing_sign_raw", + kind: WireKind::Request { + request_id: 114, + response_id: 115, + }, + }, + WireEntry { + method: "signing_sign_payload", + kind: WireKind::Request { + request_id: 116, + response_id: 117, + }, + }, + WireEntry { + method: "payment_balance_subscribe", + kind: WireKind::Subscription { + start_id: 118, + stop_id: 119, + interrupt_id: 120, + receive_id: 121, + }, + }, + WireEntry { + method: "payment_top_up", + kind: WireKind::Request { + request_id: 122, + response_id: 123, + }, + }, + WireEntry { + method: "payment_request", + kind: WireKind::Request { + request_id: 124, + response_id: 125, + }, + }, + WireEntry { + method: "payment_status_subscribe", + kind: WireKind::Subscription { + start_id: 126, + stop_id: 127, + interrupt_id: 128, + receive_id: 129, + }, + }, + WireEntry { + method: "resource_allocation_request", + kind: WireKind::Request { + request_id: 130, + response_id: 131, + }, + }, + WireEntry { + method: "statement_store_create_proof_authorized", + kind: WireKind::Request { + request_id: 132, + response_id: 133, + }, + }, +]; diff --git a/rust/crates/truapi-server/src/host_logic/dotns.rs b/rust/crates/truapi-server/src/host_logic/dotns.rs new file mode 100644 index 00000000..9a81c6b0 --- /dev/null +++ b/rust/crates/truapi-server/src/host_logic/dotns.rs @@ -0,0 +1,546 @@ +//! dotns URL parsing, normalization, and classification. +//! +//! The Rust core owns the whole decision so every platform host sees the +//! same categorization and the `navigate_to` callback only receives +//! already-validated input. + +use unicode_normalization::UnicodeNormalization; +use url::Url; + +/// How the input URL should be opened. Kept in one enum rather than passing +/// a raw string so the dispatcher can reject invalid input before reaching +/// any platform callback. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum NavigateDecision { + /// A `.dot` identifier plus path/query/hash suffix (no leading `/`). + DotName { + /// Lower-cased `.dot` host (e.g. `mytestapp.dot`). + identifier: String, + /// Path/query/hash suffix without a leading `/`. + path: String, + }, + /// A `localhost[:port]` URL plus path/query/hash suffix (no leading `/`). + Localhost { + /// `localhost` with optional `:port` suffix. + host: String, + /// Path/query/hash suffix without a leading `/`. + path: String, + }, + /// An absolute external URL with an `http(s):` scheme prepended if missing. + External { + /// Canonical URL string. + url: String, + }, + /// Input that fails every branch: empty, unparseable, or a `.dot` URL + /// carrying port/userinfo (both forbidden since dotns resolves via the + /// chain and has no notion of either). + Reject { + /// Human-readable reason for the rejection. + reason: String, + }, +} + +impl NavigateDecision { + /// Canonical URL string for the three `Open*` variants; `None` for + /// `Reject`. `DotName` and `Localhost` keep the dotns/localhost identity + /// visible so env-aware hosts (e.g. dotli rewriting `.dot` to `.dot.li`) + /// can re-parse and do their own assembly without losing information. + pub fn canonical_url(&self) -> Option { + match self { + Self::DotName { identifier, path } => Some(join_url("https://", identifier, path)), + Self::Localhost { host, path } => Some(join_url("http://", host, path)), + Self::External { url } => Some(url.clone()), + Self::Reject { .. } => None, + } + } +} + +fn join_url(scheme: &str, host: &str, path: &str) -> String { + if path.is_empty() { + format!("{scheme}{host}") + } else { + format!("{scheme}{host}/{path}") + } +} + +/// Classify a URL the way dotli's `handleNavigateTo` does: try `.dot` first, +/// then `localhost`, then normalize as external. +pub fn parse_navigate(input: &str) -> NavigateDecision { + let trimmed = input.trim(); + if trimmed.is_empty() { + return NavigateDecision::Reject { + reason: "empty input".to_string(), + }; + } + + if let Some(decision) = classify_dot(trimmed) { + return decision; + } + + if let Some(decision) = classify_localhost(trimmed) { + return decision; + } + + match normalize_external(trimmed) { + Ok(url) => NavigateDecision::External { url }, + Err(reason) => NavigateDecision::Reject { reason }, + } +} + +/// `.dot` TLD check: NFC-normalized and case-folded so `Example.DOT` and +/// `example.dot` collapse to the same outcome. +fn is_dot_domain(host: &str) -> bool { + let normalized: String = host.nfc().collect::().to_lowercase(); + normalized.ends_with(".dot") +} + +fn parse_with_explicit_https(input: &str) -> Option { + if let Ok(direct) = Url::parse(input) { + return Some(direct); + } + Url::parse(&format!("https://{input}")).ok() +} + +/// Recognize `.dot` URLs (including the `polkadot://` scheme). Returns: +/// - `Some(DotName)` for a clean `.dot` URL +/// - `Some(Reject)` for a `.dot` URL with port or userinfo +/// - `None` when the input isn't a `.dot` URL (caller falls through to +/// localhost / external) +fn classify_dot(input: &str) -> Option { + let parsed = if input.starts_with("polkadot://") { + Url::parse(input).ok()? + } else { + parse_with_explicit_https(input)? + }; + + let hostname = parsed.host_str()?; + if !is_dot_domain(hostname) { + return None; + } + + if parsed.port().is_some() || !parsed.username().is_empty() || parsed.password().is_some() { + return Some(NavigateDecision::Reject { + reason: format!("{hostname} carries port or userinfo; dotns forbids both"), + }); + } + + Some(NavigateDecision::DotName { + identifier: hostname.nfc().collect::().to_lowercase(), + path: strip_leading_slash(parsed.path()) + &suffix(&parsed), + }) +} + +/// Recognize `localhost[:port]` URLs, with or without an explicit scheme. +fn classify_localhost(input: &str) -> Option { + let with_scheme = if input.starts_with("localhost") { + format!("http://{input}") + } else { + input.to_string() + }; + + let parsed = Url::parse(&with_scheme).ok()?; + if parsed.host_str()? != "localhost" { + return None; + } + + let host = match parsed.port() { + Some(port) => format!("localhost:{port}"), + None => "localhost".to_string(), + }; + + Some(NavigateDecision::Localhost { + host, + path: strip_leading_slash(parsed.path()) + &suffix(&parsed), + }) +} + +/// External URL scheme allowlist. Anything outside this set is treated as +/// a [`NavigateDecision::Reject`] so dangerous schemes (`javascript:`, +/// `data:`, `file:`, `vbscript:`, ...) cannot reach `Platform::navigate_to`. +const ALLOWED_EXTERNAL_SCHEMES: &[&str] = &["http", "https", "mailto", "tel", "polkadot", "dot"]; + +/// Mirrors `normalizeUrl`: prepend `https://` if missing, otherwise pass the +/// URL through as its canonical string form. Returns `Err(reason)` for an +/// unparseable input or a scheme outside [`ALLOWED_EXTERNAL_SCHEMES`]. +fn normalize_external(input: &str) -> Result { + // `parse_with_explicit_https` first tries direct parse, then prepends + // `https://`. If the direct parse succeeded but produced a disallowed + // scheme, reject early so we never silently rewrite (e.g.) `javascript:` + // into `https://javascript:...`. + if let Ok(direct) = Url::parse(input) + && !ALLOWED_EXTERNAL_SCHEMES.contains(&direct.scheme()) + { + return Err(format!("scheme `{}` is not allowed", direct.scheme())); + } + let url = parse_with_explicit_https(input) + .ok_or_else(|| "URL constructor rejected input".to_string())?; + if !ALLOWED_EXTERNAL_SCHEMES.contains(&url.scheme()) { + return Err(format!("scheme `{}` is not allowed", url.scheme())); + } + Ok(url.to_string()) +} + +fn strip_leading_slash(path: &str) -> String { + path.strip_prefix('/').unwrap_or(path).to_string() +} + +fn suffix(url: &Url) -> String { + let mut out = String::new(); + if let Some(q) = url.query() { + out.push('?'); + out.push_str(q); + } + if let Some(f) = url.fragment() { + out.push('#'); + out.push_str(f); + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + fn dot(identifier: &str, path: &str) -> NavigateDecision { + NavigateDecision::DotName { + identifier: identifier.to_string(), + path: path.to_string(), + } + } + + fn localhost(host: &str, path: &str) -> NavigateDecision { + NavigateDecision::Localhost { + host: host.to_string(), + path: path.to_string(), + } + } + + fn external(url: &str) -> NavigateDecision { + NavigateDecision::External { + url: url.to_string(), + } + } + + #[test] + fn dot_bare() { + assert_eq!(parse_navigate("mytestapp.dot"), dot("mytestapp.dot", "")); + } + + #[test] + fn dot_li_is_not_a_product() { + assert_eq!( + parse_navigate("mytestapp.dot.li"), + external("https://mytestapp.dot.li/") + ); + } + + #[test] + fn dot_with_https() { + assert_eq!( + parse_navigate("https://mytestapp.dot"), + dot("mytestapp.dot", "") + ); + } + + #[test] + fn dot_with_http() { + assert_eq!( + parse_navigate("http://mytestapp.dot"), + dot("mytestapp.dot", "") + ); + } + + #[test] + fn dot_with_path() { + assert_eq!( + parse_navigate("mytestapp.dot/some/path"), + dot("mytestapp.dot", "some/path") + ); + } + + #[test] + fn dot_with_query_only() { + assert_eq!( + parse_navigate("pr508.faucet.dot?embed=1"), + dot("pr508.faucet.dot", "?embed=1") + ); + } + + #[test] + fn dot_with_hash_only() { + assert_eq!( + parse_navigate("pr508.faucet.dot#section=main"), + dot("pr508.faucet.dot", "#section=main") + ); + } + + #[test] + fn dot_with_path_query_hash() { + assert_eq!( + parse_navigate("pr508.faucet.dot/nested/path?embed=1#frame=compact"), + dot("pr508.faucet.dot", "nested/path?embed=1#frame=compact") + ); + } + + #[test] + fn polkadot_scheme_dot_host() { + assert_eq!( + parse_navigate("polkadot://currenthost.dot/mytestapp.dot"), + dot("currenthost.dot", "mytestapp.dot") + ); + } + + #[test] + fn polkadot_scheme_non_dot_host_falls_through() { + match parse_navigate("polkadot://example.com/settings") { + NavigateDecision::External { .. } | NavigateDecision::Reject { .. } => {} + other => panic!("expected External or Reject, got {other:?}"), + } + } + + #[test] + fn polkadot_scheme_with_path() { + assert_eq!( + parse_navigate("polkadot://currenthost.dot/mytestapp.dot/settings"), + dot("currenthost.dot", "mytestapp.dot/settings") + ); + } + + #[test] + fn polkadot_scheme_with_query_and_hash() { + assert_eq!( + parse_navigate("polkadot://currenthost.dot/mytestapp.dot?embed=1#frame=compact"), + dot("currenthost.dot", "mytestapp.dot?embed=1#frame=compact") + ); + } + + #[test] + fn dot_subdomain() { + assert_eq!( + parse_navigate("sub.acme.dot/path"), + dot("sub.acme.dot", "path") + ); + } + + #[test] + fn dot_with_mixed_case_normalizes() { + assert_eq!( + parse_navigate("Example.DOT/Path"), + dot("example.dot", "Path") + ); + } + + #[test] + fn dot_with_port_is_rejected() { + assert!(matches!( + parse_navigate("https://x.dot:8080/path"), + NavigateDecision::Reject { .. } + )); + } + + #[test] + fn dot_with_userinfo_is_rejected() { + assert!(matches!( + parse_navigate("https://user:pass@x.dot/path"), + NavigateDecision::Reject { .. } + )); + } + + #[test] + fn trim_whitespace() { + assert_eq!( + parse_navigate(" mytestapp.dot/path "), + dot("mytestapp.dot", "path") + ); + } + + #[test] + fn localhost_bare_with_port() { + assert_eq!( + parse_navigate("localhost:3000"), + localhost("localhost:3000", "") + ); + } + + #[test] + fn localhost_with_port_and_path() { + assert_eq!( + parse_navigate("localhost:3000/some/path"), + localhost("localhost:3000", "some/path") + ); + } + + #[test] + fn localhost_with_explicit_http() { + assert_eq!( + parse_navigate("http://localhost:5000"), + localhost("localhost:5000", "") + ); + } + + #[test] + fn localhost_with_http_and_path() { + assert_eq!( + parse_navigate("http://localhost:5000/path"), + localhost("localhost:5000", "path") + ); + } + + #[test] + fn localhost_with_query_and_hash() { + assert_eq!( + parse_navigate("localhost:3000/path?q=1#h"), + localhost("localhost:3000", "path?q=1#h") + ); + } + + #[test] + fn localhost_without_port() { + assert_eq!(parse_navigate("localhost"), localhost("localhost", "")); + } + + #[test] + fn localhost_without_port_with_path() { + assert_eq!( + parse_navigate("localhost/path"), + localhost("localhost", "path") + ); + } + + #[test] + fn external_bare_domain() { + assert_eq!( + parse_navigate("google.com"), + external("https://google.com/") + ); + } + + #[test] + fn external_bare_domain_with_path() { + assert_eq!( + parse_navigate("google.com/search?q=test"), + external("https://google.com/search?q=test") + ); + } + + #[test] + fn external_preserves_https() { + assert_eq!( + parse_navigate("https://example.com/page"), + external("https://example.com/page") + ); + } + + #[test] + fn external_preserves_http() { + assert_eq!( + parse_navigate("http://example.com/page"), + external("http://example.com/page") + ); + } + + #[test] + fn external_dot_li() { + assert_eq!( + parse_navigate("acme.dot.li/path/1"), + external("https://acme.dot.li/path/1") + ); + } + + #[test] + fn reject_empty() { + assert!(matches!( + parse_navigate(""), + NavigateDecision::Reject { .. } + )); + } + + #[test] + fn reject_whitespace() { + assert!(matches!( + parse_navigate(" "), + NavigateDecision::Reject { .. } + )); + } + + #[test] + fn reject_unparseable() { + assert!(matches!( + parse_navigate(":::invalid"), + NavigateDecision::Reject { .. } + )); + } + + /// `javascript:` URIs must never reach the platform's `navigate_to`; + /// otherwise a malicious product could execute arbitrary JS in the host. + #[test] + fn reject_javascript_uri() { + assert!( + matches!( + parse_navigate("javascript:alert(1)"), + NavigateDecision::Reject { .. } + ), + "javascript: scheme must be rejected" + ); + } + + /// `file:` URIs leak local filesystem paths; reject them. + #[test] + fn reject_file_uri() { + assert!( + matches!( + parse_navigate("file:///etc/passwd"), + NavigateDecision::Reject { .. } + ), + "file: scheme must be rejected" + ); + } + + /// `data:` URIs can carry inline HTML/JS payloads; reject them. + #[test] + fn reject_data_uri() { + assert!( + matches!( + parse_navigate("data:text/html,"), + NavigateDecision::Reject { .. } + ), + "data: scheme must be rejected" + ); + } + + /// `vbscript:` URIs are the legacy IE equivalent of `javascript:`; + /// reject them too even though modern browsers don't execute them. + #[test] + fn reject_vbscript_uri() { + assert!( + matches!( + parse_navigate("vbscript:msgbox(1)"), + NavigateDecision::Reject { .. } + ), + "vbscript: scheme must be rejected" + ); + } + + /// NFC-normalized and NFD-normalized inputs that represent the same + /// dotns name must produce the same `DotName.identifier` so downstream + /// resolution can't be fooled into looking up two different lookup keys + /// for one visual identity. + #[test] + fn nfc_normalization_collapses_nfd() { + let nfc = parse_navigate("café.dot"); + let nfd = parse_navigate("cafe\u{0301}.dot"); + match (&nfc, &nfd) { + ( + NavigateDecision::DotName { + identifier: a, + path: _, + }, + NavigateDecision::DotName { + identifier: b, + path: _, + }, + ) => assert_eq!(a, b, "NFC and NFD inputs must normalize to one identifier"), + other => panic!("expected two DotName decisions, got {other:?}"), + } + } +} diff --git a/rust/crates/truapi-server/src/host_logic/features.rs b/rust/crates/truapi-server/src/host_logic/features.rs new file mode 100644 index 00000000..e775783b --- /dev/null +++ b/rust/crates/truapi-server/src/host_logic/features.rs @@ -0,0 +1,76 @@ +//! Feature-detection delegation. +//! +//! Unlike older drafts that baked a genesis-hash allow-list into core, the +//! v0.1 surface treats `feature_supported` as a pure platform syscall: each +//! host owns the set of chains it can service. This module is a thin shim +//! that forwards the request through to [`truapi_platform::Features`]. + +use truapi::v01::GenericError; +use truapi::versioned::system::{HostFeatureSupportedRequest, HostFeatureSupportedResponse}; +use truapi_platform::Features; + +/// Forward a feature-support query to the platform implementation. +pub async fn feature_supported( + platform: &P, + request: HostFeatureSupportedRequest, +) -> Result { + platform.feature_supported(request).await +} + +#[cfg(test)] +mod tests { + use super::*; + use async_trait::async_trait; + use truapi::v01; + + struct AlwaysSupported; + + #[async_trait] + impl Features for AlwaysSupported { + async fn feature_supported( + &self, + request: HostFeatureSupportedRequest, + ) -> Result { + let HostFeatureSupportedRequest::V1(_) = request; + Ok(HostFeatureSupportedResponse::V1( + v01::HostFeatureSupportedResponse { supported: true }, + )) + } + } + + struct AlwaysUnsupported; + + #[async_trait] + impl Features for AlwaysUnsupported { + async fn feature_supported( + &self, + request: HostFeatureSupportedRequest, + ) -> Result { + let HostFeatureSupportedRequest::V1(_) = request; + Ok(HostFeatureSupportedResponse::V1( + v01::HostFeatureSupportedResponse { supported: false }, + )) + } + } + + fn req() -> HostFeatureSupportedRequest { + HostFeatureSupportedRequest::V1(v01::HostFeatureSupportedRequest::Chain { + genesis_hash: vec![0u8; 32], + }) + } + + #[test] + fn delegates_supported_to_platform() { + let resp = futures::executor::block_on(feature_supported(&AlwaysSupported, req())).unwrap(); + let HostFeatureSupportedResponse::V1(inner) = resp; + assert!(inner.supported); + } + + #[test] + fn delegates_unsupported_to_platform() { + let resp = + futures::executor::block_on(feature_supported(&AlwaysUnsupported, req())).unwrap(); + let HostFeatureSupportedResponse::V1(inner) = resp; + assert!(!inner.supported); + } +} diff --git a/rust/crates/truapi-server/src/host_logic/mod.rs b/rust/crates/truapi-server/src/host_logic/mod.rs new file mode 100644 index 00000000..a96cd637 --- /dev/null +++ b/rust/crates/truapi-server/src/host_logic/mod.rs @@ -0,0 +1,10 @@ +//! Host-agnostic logic the Rust core owns on behalf of every platform host. +//! +//! Platform callbacks are a syscall layer for OS primitives (modals, native +//! storage, URL handler, notification center). Everything else lives here so +//! iOS, Android, and web hosts share one canonical implementation. + +pub mod dotns; +pub mod features; +pub mod permissions; +pub mod session; diff --git a/rust/crates/truapi-server/src/host_logic/permissions.rs b/rust/crates/truapi-server/src/host_logic/permissions.rs new file mode 100644 index 00000000..bc7d19c6 --- /dev/null +++ b/rust/crates/truapi-server/src/host_logic/permissions.rs @@ -0,0 +1,387 @@ +//! Permission state machine (ask -> granted | denied), backed by the platform +//! [`Storage`] trait with a reserved `truapi:permissions:` key prefix. +//! +//! The v0.1 wire protocol keeps device permissions (camera, mic, NFC, ...) +//! separate from remote permissions (domain access, chain submit, ...), so +//! this module exposes two `check_or_prompt` entrypoints that route to the +//! matching platform callback. The cache layer is shared but keys live in +//! distinct sub-namespaces so a device grant cannot authorize a remote +//! operation by accident. + +use parity_scale_codec::{Decode, Encode}; + +use truapi::v01::{ + HostDevicePermissionRequest, HostDevicePermissionResponse, RemotePermission, + RemotePermissionRequest, RemotePermissionResponse, +}; +use truapi_platform::{Permissions, Storage, StorageError}; + +/// Reserved key prefix for permission state. Hosts must not use keys under +/// this prefix for anything else so core can own the namespace. +pub const PERMISSION_KEY_PREFIX: &str = "truapi:permissions:"; + +/// Persisted answer for a single permission request. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] +pub enum Decision { + /// User granted the permission. + Granted, + /// User denied the permission. + Denied, +} + +/// Coordinator that inspects persisted state first, falls back to the +/// platform's prompt callback, and writes the decision back so future calls +/// short-circuit. +pub struct PermissionsService<'a> { + storage: &'a dyn Storage, + prompt: &'a dyn Permissions, +} + +impl<'a> PermissionsService<'a> { + /// Construct a service backed by the given storage + prompt callbacks. + pub fn new(storage: &'a dyn Storage, prompt: &'a dyn Permissions) -> Self { + Self { storage, prompt } + } + + /// Returns the stored decision for a device permission without prompting. + pub async fn peek_device( + &self, + permission: &HostDevicePermissionRequest, + ) -> Result, StorageError> { + peek(self.storage, &device_storage_key(permission)).await + } + + /// Returns the stored decision for a remote permission bundle without + /// prompting. + pub async fn peek_remote( + &self, + request: &RemotePermissionRequest, + ) -> Result, StorageError> { + peek(self.storage, &remote_storage_key(request)).await + } + + /// Returns the cached device decision if any, otherwise prompts the + /// platform's `device_permission` callback and persists the answer. + pub async fn check_or_prompt_device( + &self, + permission: HostDevicePermissionRequest, + ) -> Result { + let key = device_storage_key(&permission); + if let Some(cached) = peek(self.storage, &key).await? { + return Ok(cached); + } + let granted = match self.prompt.device_permission(permission).await { + Ok(HostDevicePermissionResponse { granted }) => granted, + Err(_) => false, + }; + let decision = if granted { + Decision::Granted + } else { + Decision::Denied + }; + self.storage.write(key, decision.encode()).await?; + Ok(decision) + } + + /// Returns the cached remote decision if any, otherwise prompts the + /// platform's `remote_permission` callback and persists the answer. + pub async fn check_or_prompt_remote( + &self, + request: RemotePermissionRequest, + ) -> Result { + let key = remote_storage_key(&request); + if let Some(cached) = peek(self.storage, &key).await? { + return Ok(cached); + } + let granted = match self.prompt.remote_permission(request).await { + Ok(RemotePermissionResponse { granted }) => granted, + Err(_) => false, + }; + let decision = if granted { + Decision::Granted + } else { + Decision::Denied + }; + self.storage.write(key, decision.encode()).await?; + Ok(decision) + } +} + +async fn peek(storage: &dyn Storage, key: &str) -> Result, StorageError> { + let Some(raw) = storage.read(key.to_string()).await? else { + return Ok(None); + }; + Ok(Decision::decode(&mut &*raw).ok()) +} + +/// Canonical storage key for a device permission. The slug is human-readable +/// so a host developer inspecting storage can tell what's there. +pub fn device_storage_key(permission: &HostDevicePermissionRequest) -> String { + format!("{PERMISSION_KEY_PREFIX}device:{}", device_slug(permission)) +} + +/// Canonical storage key for a remote permission bundle. Permissions inside +/// the bundle are sorted so equivalent batches (same set, different order) +/// collapse to one storage entry. +pub fn remote_storage_key(request: &RemotePermissionRequest) -> String { + let mut slugs: Vec = request.permissions.iter().map(remote_slug).collect(); + slugs.sort(); + format!("{PERMISSION_KEY_PREFIX}remote:{}", slugs.join("|")) +} + +fn device_slug(permission: &HostDevicePermissionRequest) -> &'static str { + match permission { + HostDevicePermissionRequest::Notifications => "notifications", + HostDevicePermissionRequest::Camera => "camera", + HostDevicePermissionRequest::Microphone => "microphone", + HostDevicePermissionRequest::Bluetooth => "bluetooth", + HostDevicePermissionRequest::NFC => "nfc", + HostDevicePermissionRequest::Location => "location", + HostDevicePermissionRequest::Clipboard => "clipboard", + HostDevicePermissionRequest::OpenUrl => "open-url", + HostDevicePermissionRequest::Biometrics => "biometrics", + } +} + +fn remote_slug(permission: &RemotePermission) -> String { + match permission { + RemotePermission::Remote { domains } => { + let mut sorted = domains.clone(); + sorted.sort(); + format!("domains:{}", sorted.join(",")) + } + RemotePermission::WebRtc => "web-rtc".to_string(), + RemotePermission::ChainSubmit => "chain-submit".to_string(), + RemotePermission::PreimageSubmit => "preimage-submit".to_string(), + RemotePermission::StatementSubmit => "statement-submit".to_string(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use async_trait::async_trait; + use futures::lock::Mutex; + use std::collections::HashMap; + use std::sync::atomic::{AtomicUsize, Ordering}; + use truapi::v01; + use truapi::v01::GenericError; + use truapi_platform::{StorageKey, StorageValue}; + + #[derive(Default)] + struct MemStorage { + inner: Mutex>, + } + + #[async_trait] + impl Storage for MemStorage { + async fn read(&self, key: StorageKey) -> Result, StorageError> { + Ok(self.inner.lock().await.get(&key).cloned()) + } + async fn write(&self, key: StorageKey, value: StorageValue) -> Result<(), StorageError> { + self.inner.lock().await.insert(key, value); + Ok(()) + } + async fn clear(&self, key: StorageKey) -> Result<(), StorageError> { + self.inner.lock().await.remove(&key); + Ok(()) + } + } + + struct ScriptedPrompt { + device_answers: Mutex>, + remote_answers: Mutex>, + device_calls: AtomicUsize, + remote_calls: AtomicUsize, + } + + impl ScriptedPrompt { + fn new(device_answers: Vec, remote_answers: Vec) -> Self { + Self { + device_answers: Mutex::new(device_answers), + remote_answers: Mutex::new(remote_answers), + device_calls: AtomicUsize::new(0), + remote_calls: AtomicUsize::new(0), + } + } + } + + #[async_trait] + impl Permissions for ScriptedPrompt { + async fn device_permission( + &self, + _request: HostDevicePermissionRequest, + ) -> Result { + self.device_calls.fetch_add(1, Ordering::SeqCst); + let granted = self + .device_answers + .lock() + .await + .pop() + .expect("ScriptedPrompt ran out of device answers"); + Ok(v01::HostDevicePermissionResponse { granted }) + } + + async fn remote_permission( + &self, + _request: RemotePermissionRequest, + ) -> Result { + self.remote_calls.fetch_add(1, Ordering::SeqCst); + let granted = self + .remote_answers + .lock() + .await + .pop() + .expect("ScriptedPrompt ran out of remote answers"); + Ok(v01::RemotePermissionResponse { granted }) + } + } + + #[test] + fn storage_key_is_stable_per_variant() { + assert_eq!( + device_storage_key(&HostDevicePermissionRequest::Camera), + "truapi:permissions:device:camera", + ); + let chain = RemotePermissionRequest { + permissions: vec![RemotePermission::ChainSubmit], + }; + assert_eq!( + remote_storage_key(&chain), + "truapi:permissions:remote:chain-submit", + ); + let domains = RemotePermissionRequest { + permissions: vec![RemotePermission::Remote { + domains: vec!["b.example.com".into(), "a.example.com".into()], + }], + }; + assert_eq!( + remote_storage_key(&domains), + "truapi:permissions:remote:domains:a.example.com,b.example.com", + ); + } + + #[test] + fn remote_storage_key_sorts_bundle() { + let a = RemotePermissionRequest { + permissions: vec![RemotePermission::WebRtc, RemotePermission::ChainSubmit], + }; + let b = RemotePermissionRequest { + permissions: vec![RemotePermission::ChainSubmit, RemotePermission::WebRtc], + }; + assert_eq!(remote_storage_key(&a), remote_storage_key(&b)); + } + + #[test] + fn check_or_prompt_device_caches_grant() { + let storage = MemStorage::default(); + let prompt = ScriptedPrompt::new(vec![true], vec![]); + let service = PermissionsService::new(&storage, &prompt); + + let first = futures::executor::block_on( + service.check_or_prompt_device(HostDevicePermissionRequest::Camera), + ) + .unwrap(); + let second = futures::executor::block_on( + service.check_or_prompt_device(HostDevicePermissionRequest::Camera), + ) + .unwrap(); + + assert_eq!(first, Decision::Granted); + assert_eq!(second, Decision::Granted); + assert_eq!(prompt.device_calls.load(Ordering::SeqCst), 1); + } + + #[test] + fn check_or_prompt_remote_caches_denial() { + let storage = MemStorage::default(); + let prompt = ScriptedPrompt::new(vec![], vec![false]); + let service = PermissionsService::new(&storage, &prompt); + + let request = RemotePermissionRequest { + permissions: vec![RemotePermission::ChainSubmit], + }; + let first = + futures::executor::block_on(service.check_or_prompt_remote(request.clone())).unwrap(); + let second = futures::executor::block_on(service.check_or_prompt_remote(request)).unwrap(); + + assert_eq!(first, Decision::Denied); + assert_eq!(second, Decision::Denied); + assert_eq!(prompt.remote_calls.load(Ordering::SeqCst), 1); + } + + #[test] + fn device_and_remote_caches_are_independent() { + let storage = MemStorage::default(); + // Device denies, remote grants. If the caches collided we'd see the + // same answer on the second call. + let prompt = ScriptedPrompt::new(vec![false], vec![true]); + let service = PermissionsService::new(&storage, &prompt); + + let device = futures::executor::block_on( + service.check_or_prompt_device(HostDevicePermissionRequest::Camera), + ) + .unwrap(); + let remote = + futures::executor::block_on(service.check_or_prompt_remote(RemotePermissionRequest { + permissions: vec![RemotePermission::ChainSubmit], + })) + .unwrap(); + + assert_eq!(device, Decision::Denied); + assert_eq!(remote, Decision::Granted); + assert_eq!(prompt.device_calls.load(Ordering::SeqCst), 1); + assert_eq!(prompt.remote_calls.load(Ordering::SeqCst), 1); + } + + #[test] + fn device_prompt_does_not_invoke_remote_callback() { + let storage = MemStorage::default(); + let prompt = ScriptedPrompt::new(vec![true], vec![]); + let service = PermissionsService::new(&storage, &prompt); + + let _ = futures::executor::block_on( + service.check_or_prompt_device(HostDevicePermissionRequest::Camera), + ) + .unwrap(); + assert_eq!(prompt.device_calls.load(Ordering::SeqCst), 1); + assert_eq!(prompt.remote_calls.load(Ordering::SeqCst), 0); + } + + #[test] + fn remote_prompt_does_not_invoke_device_callback() { + let storage = MemStorage::default(); + let prompt = ScriptedPrompt::new(vec![], vec![true]); + let service = PermissionsService::new(&storage, &prompt); + + let _ = + futures::executor::block_on(service.check_or_prompt_remote(RemotePermissionRequest { + permissions: vec![RemotePermission::WebRtc], + })) + .unwrap(); + assert_eq!(prompt.device_calls.load(Ordering::SeqCst), 0); + assert_eq!(prompt.remote_calls.load(Ordering::SeqCst), 1); + } + + #[test] + fn peek_returns_none_until_decided() { + let storage = MemStorage::default(); + let prompt = ScriptedPrompt::new(vec![true], vec![]); + let service = PermissionsService::new(&storage, &prompt); + + let before = + futures::executor::block_on(service.peek_device(&HostDevicePermissionRequest::Camera)) + .unwrap(); + assert_eq!(before, None); + + futures::executor::block_on( + service.check_or_prompt_device(HostDevicePermissionRequest::Camera), + ) + .unwrap(); + + let after = + futures::executor::block_on(service.peek_device(&HostDevicePermissionRequest::Camera)) + .unwrap(); + assert_eq!(after, Some(Decision::Granted)); + } +} diff --git a/rust/crates/truapi-server/src/host_logic/session.rs b/rust/crates/truapi-server/src/host_logic/session.rs new file mode 100644 index 00000000..142b1b9f --- /dev/null +++ b/rust/crates/truapi-server/src/host_logic/session.rs @@ -0,0 +1,233 @@ +//! Active-session state held in core. The host pushes session info via +//! platform-specific entrypoints whenever the user pairs/unpairs. +//! Account-management methods then read from this state instead of +//! round-tripping a callback to the host on every product call. + +use futures::channel::mpsc; +use futures::stream::{self, BoxStream, StreamExt}; +use std::sync::{Arc, Mutex}; + +use truapi::v01::HostAccountConnectionStatusSubscribeItem; +use truapi::versioned::account::HostAccountConnectionStatusSubscribeItem as VersionedItem; + +/// Session info pushed by the host. The 32-byte sr25519 public key plus +/// optional usernames sourced from the People-Chain identity record. +#[derive(Debug, Clone)] +pub struct SessionInfo { + /// 32-byte sr25519 root public key of the paired session. + pub public_key: [u8; 32], + /// Short username (e.g. `alice`). + pub lite_username: Option, + /// Fully qualified username (e.g. `Alice Smith`). + pub full_username: Option, +} + +/// Holds the currently-active session and broadcasts connection-status +/// transitions to subscribers. Cheap to clone via `Arc`. +#[derive(Default)] +pub struct SessionState { + inner: Mutex, +} + +#[derive(Default)] +struct Inner { + current: Option, + subscribers: Vec>, +} + +impl SessionState { + /// Construct a fresh session holder, starting in the `Disconnected` state. + pub fn new() -> Arc { + Arc::new(Self::default()) + } + + /// Replace the active session with `info`. Emits a `Connected` event to + /// every live subscriber if this is a transition from no-session. + pub fn set_session(&self, info: SessionInfo) { + let mut inner = self.inner.lock().expect("session-state mutex poisoned"); + let was_present = inner.current.is_some(); + inner.current = Some(info); + if !was_present { + broadcast( + &mut inner.subscribers, + HostAccountConnectionStatusSubscribeItem::Connected, + ); + } + } + + /// Drop the active session. Emits a `Disconnected` event to every live + /// subscriber if there was a session to clear. + pub fn clear_session(&self) { + let mut inner = self.inner.lock().expect("session-state mutex poisoned"); + if inner.current.take().is_some() { + broadcast( + &mut inner.subscribers, + HostAccountConnectionStatusSubscribeItem::Disconnected, + ); + } + } + + /// Snapshot of the current session, or `None` when nothing is paired. + pub fn current(&self) -> Option { + self.inner + .lock() + .expect("session-state mutex poisoned") + .current + .clone() + } + + /// Stream of connection-status events. The first item emitted is the + /// current state (so subscribers don't have to read it separately); + /// subsequent items reflect every `set_session` / `clear_session` + /// transition. + pub fn subscribe(&self) -> BoxStream<'static, VersionedItem> { + let (tx, rx) = mpsc::unbounded(); + let mut inner = self.inner.lock().expect("session-state mutex poisoned"); + let initial = match inner.current { + Some(_) => HostAccountConnectionStatusSubscribeItem::Connected, + None => HostAccountConnectionStatusSubscribeItem::Disconnected, + }; + inner.subscribers.push(tx); + let initial_item = VersionedItem::V1(initial); + Box::pin(stream::once(async move { initial_item }).chain(rx)) + } +} + +fn broadcast( + subscribers: &mut Vec>, + status: HostAccountConnectionStatusSubscribeItem, +) { + let item = VersionedItem::V1(status); + // `retain` drops senders whose receiver has been dropped, so the + // subscriber list self-prunes on the next broadcast after a reader + // unsubscribes. + subscribers.retain(|tx| tx.unbounded_send(item.clone()).is_ok()); +} + +#[cfg(test)] +mod tests { + use super::*; + use futures::executor::block_on; + use futures::{FutureExt, StreamExt}; + + fn info(pubkey_byte: u8) -> SessionInfo { + SessionInfo { + public_key: [pubkey_byte; 32], + lite_username: Some("alice".to_string()), + full_username: None, + } + } + + #[test] + fn current_starts_empty() { + let state = SessionState::new(); + assert!(state.current().is_none()); + } + + #[test] + fn set_then_current_returns_session() { + let state = SessionState::new(); + state.set_session(info(0x42)); + let got = state.current().expect("session should be present"); + assert_eq!(got.public_key, [0x42; 32]); + assert_eq!(got.lite_username.as_deref(), Some("alice")); + } + + #[test] + fn clear_returns_to_empty() { + let state = SessionState::new(); + state.set_session(info(0x01)); + state.clear_session(); + assert!(state.current().is_none()); + } + + #[test] + fn subscribe_emits_current_state_first() { + let state = SessionState::new(); + state.set_session(info(0x01)); + let mut stream = state.subscribe(); + let first = block_on(stream.next()).expect("expected initial item"); + assert_eq!( + first, + VersionedItem::V1(HostAccountConnectionStatusSubscribeItem::Connected) + ); + } + + #[test] + fn subscribe_emits_disconnected_when_no_session() { + let state = SessionState::new(); + let mut stream = state.subscribe(); + let first = block_on(stream.next()).expect("expected initial item"); + assert_eq!( + first, + VersionedItem::V1(HostAccountConnectionStatusSubscribeItem::Disconnected) + ); + } + + #[test] + fn set_session_broadcasts_connected_to_existing_subscribers() { + let state = SessionState::new(); + let mut stream = state.subscribe(); + let _ = block_on(stream.next()); + + state.set_session(info(0x01)); + let next = block_on(stream.next()).expect("expected Connected event"); + assert_eq!( + next, + VersionedItem::V1(HostAccountConnectionStatusSubscribeItem::Connected) + ); + } + + #[test] + fn clear_session_broadcasts_disconnected_to_existing_subscribers() { + let state = SessionState::new(); + state.set_session(info(0x01)); + let mut stream = state.subscribe(); + let _ = block_on(stream.next()); + + state.clear_session(); + let next = block_on(stream.next()).expect("expected Disconnected event"); + assert_eq!( + next, + VersionedItem::V1(HostAccountConnectionStatusSubscribeItem::Disconnected) + ); + } + + #[test] + fn set_session_twice_does_not_re_emit_connected() { + let state = SessionState::new(); + state.set_session(info(0x01)); + let mut stream = state.subscribe(); + let _ = block_on(stream.next()); + + state.set_session(info(0x02)); + + let pending = stream.next().now_or_never(); + assert!( + pending.is_none(), + "no transition event expected on session replace" + ); + } + + #[test] + fn multi_subscriber_broadcast() { + let state = SessionState::new(); + let mut a = state.subscribe(); + let mut b = state.subscribe(); + // Drain initial Disconnected from both. + let _ = block_on(a.next()); + let _ = block_on(b.next()); + + state.set_session(info(0x77)); + let a_next = block_on(a.next()).expect("a should receive Connected"); + let b_next = block_on(b.next()).expect("b should receive Connected"); + assert_eq!( + a_next, + VersionedItem::V1(HostAccountConnectionStatusSubscribeItem::Connected) + ); + assert_eq!( + b_next, + VersionedItem::V1(HostAccountConnectionStatusSubscribeItem::Connected) + ); + } +} diff --git a/rust/crates/truapi-server/src/lib.rs b/rust/crates/truapi-server/src/lib.rs new file mode 100644 index 00000000..bd7d7fa5 --- /dev/null +++ b/rust/crates/truapi-server/src/lib.rs @@ -0,0 +1,52 @@ +//! TrUAPI server runtime: dispatcher, frames, SCALE encoding, stream management. +//! +//! Phase 4c adds the runtime + host_logic + core layers on top of the +//! 4a skeleton. The platform path (`TrUApiCore::from_platform`) wraps a +//! [`truapi_platform::Platform`] in a `PlatformRuntimeHost` that implements +//! every `truapi::api::*` trait by delegating to platform callbacks. +//! +//! Phase 4e adds the host-facing bridges: +//! - [`ws_bridge`] (feature `ws-bridge`): localhost WebSocket bridge for +//! native WebView hosts (Android/iOS). +//! - [`native`]: UniFFI surface exposing `NativeTrUApiCore` + `HostCallbacks`. +//! - [`wasm`] (wasm32 only): wasm-bindgen surface exposing `WasmTrUApiCore`. + +#![forbid(unsafe_code)] + +pub mod core; +pub mod debug_log; +pub mod dispatcher; +pub mod frame; +pub mod host_logic; +pub mod runtime; +pub mod subscription; +pub mod transport; + +pub mod generated; + +#[cfg(all(not(target_arch = "wasm32"), feature = "ws-bridge"))] +pub mod ws_bridge; + +#[cfg(not(target_arch = "wasm32"))] +pub mod native; + +#[cfg(target_arch = "wasm32")] +pub mod wasm; + +pub use crate::core::TrUApiCore; +pub use dispatcher::*; +pub use frame::*; +pub use runtime::PlatformRuntimeHost; +pub use subscription::*; +pub use transport::*; + +#[cfg(all(not(target_arch = "wasm32"), feature = "ws-bridge"))] +pub use ws_bridge::*; + +#[cfg(not(target_arch = "wasm32"))] +pub use native::*; + +#[cfg(target_arch = "wasm32")] +pub use wasm::*; + +uniffi::setup_scaffolding!(); diff --git a/rust/crates/truapi-server/src/native.rs b/rust/crates/truapi-server/src/native.rs new file mode 100644 index 00000000..74c792f2 --- /dev/null +++ b/rust/crates/truapi-server/src/native.rs @@ -0,0 +1,706 @@ +//! UniFFI-facing native bridge. Exposes [`NativeTrUApiCore`] and the +//! [`HostCallbacks`] callback interface that iOS and Android call into. +//! +//! The native side builds a [`CallbackPlatform`] that adapts every +//! [`truapi_platform::Platform`] trait to a corresponding callback. The +//! resulting platform is fed into [`TrUApiCore::from_platform`] so the rest +//! of the dispatcher pipeline behaves identically to the WS-bridge and wasm +//! flavors. +//! +//! Note on adaptation: the bridge exposes `device_permission` and +//! `remote_permission` as separate callbacks (matching the v0.1 platform +//! surface) rather than the merged `prompt_permission` shape used by older +//! prototypes. `feature_supported` and `navigate_to` are retained. + +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use async_trait::async_trait; +use futures::stream::{self, BoxStream}; +use parity_scale_codec::{Decode, Encode}; +use truapi::v01; +use truapi::versioned::account::{ + HostAccountConnectionStatusSubscribeItem, HostAccountCreateProofRequest, + HostAccountCreateProofResponse, HostAccountGetAliasRequest, HostAccountGetAliasResponse, + HostAccountGetRequest, HostAccountGetResponse, HostGetLegacyAccountsRequest, + HostGetLegacyAccountsResponse, HostGetUserIdRequest, HostGetUserIdResponse, +}; +use truapi::versioned::preimage::{ + RemotePreimageLookupSubscribeItem, RemotePreimageLookupSubscribeRequest, +}; +use truapi::versioned::signing::{ + HostSignPayloadRequest, HostSignPayloadResponse, HostSignRawRequest, HostSignRawResponse, +}; +use truapi::versioned::statement_store::{ + RemoteStatementStoreCreateProofRequest, RemoteStatementStoreCreateProofResponse, + RemoteStatementStoreSubmitRequest, RemoteStatementStoreSubscribeItem, + RemoteStatementStoreSubscribeRequest, +}; +use truapi::versioned::system::{HostFeatureSupportedRequest, HostFeatureSupportedResponse}; +use truapi_platform::{ + Accounts, ChainProvider, Features, JsonRpcConnection, Navigation, Notifications, Permissions, + Preimage, Signing, StatementStore, Storage, +}; + +#[cfg(feature = "ws-bridge")] +use crate::ws_bridge::{BridgeLogger, WsBridge, WsBridgeEndpoint, WsBridgeStartError}; +use crate::{Payload, ProtocolMessage, TrUApiCore, Transport}; + +/// Native-friendly storage error. Mirrors the v0.1 wire shape so the +/// callback surface stays SCALE-free. +#[derive(Debug, Clone, thiserror::Error, uniffi::Error)] +pub enum HostStorageError { + /// Quota exhausted. + #[error("storage quota exhausted")] + Full, + /// Catch-all. + #[error("{reason}")] + Unknown { + /// Human-readable failure reason. + reason: String, + }, +} + +impl From for v01::HostLocalStorageReadError { + fn from(err: HostStorageError) -> Self { + match err { + HostStorageError::Full => v01::HostLocalStorageReadError::Full, + HostStorageError::Unknown { reason } => { + v01::HostLocalStorageReadError::Unknown { reason } + } + } + } +} + +/// Native-friendly rejection error returned by callback methods that map +/// onto [`truapi::v01::GenericError`]. +#[derive(Debug, Clone, thiserror::Error, uniffi::Error)] +pub enum HostRejection { + /// Caller rejected the operation. + #[error("{reason}")] + Rejected { + /// Human-readable rejection reason. + reason: String, + }, +} + +impl From for v01::GenericError { + fn from(err: HostRejection) -> Self { + let HostRejection::Rejected { reason } = err; + v01::GenericError::GenericError(v01::GenericErr { reason }) + } +} + +/// Native-friendly navigation error. +#[derive(Debug, Clone, thiserror::Error, uniffi::Error)] +pub enum HostNavigateRejection { + /// User declined the navigation. + #[error("navigation denied by user")] + PermissionDenied, + /// Catch-all. + #[error("{reason}")] + Unknown { + /// Human-readable reason. + reason: String, + }, +} + +impl From for v01::HostNavigateToError { + fn from(err: HostNavigateRejection) -> Self { + match err { + HostNavigateRejection::PermissionDenied => v01::HostNavigateToError::PermissionDenied, + HostNavigateRejection::Unknown { reason } => { + v01::HostNavigateToError::Unknown { reason } + } + } + } +} + +/// Callback surface that iOS and Android implement. The Rust core invokes +/// these synchronously from `async` trait methods, which is acceptable for +/// UniFFI because every callback hop is short-lived and reentrant. +#[uniffi::export(callback_interface)] +pub trait HostCallbacks: Send + Sync { + /// Lifecycle logger. Marker is a stable slug, detail is free-form. + fn on_core_log(&self, marker: String, detail: String); + + /// Forward an outbound protocol frame (already SCALE-encoded) to the + /// product. The native shell pumps these into the in-app messaging + /// channel. + fn on_core_response(&self, frame: Vec); + + /// Open a URL in the system browser. + fn navigate_to(&self, url: String) -> Result<(), HostNavigateRejection>; + + /// Deliver a push notification. The payload is the SCALE-encoded + /// [`v01::HostPushNotificationRequest`]. + fn push_notification(&self, payload: Vec) -> Result<(), HostRejection>; + + /// Prompt the user for a device-level permission (camera, mic, ...). + /// `request` is the SCALE-encoded + /// [`v01::HostDevicePermissionRequest`]; the host returns whether the + /// permission was granted. + fn device_permission(&self, request: Vec) -> Result; + + /// Prompt the user for a remote (product-scoped) permission bundle. + /// `request` is the SCALE-encoded [`v01::RemotePermissionRequest`]. + fn remote_permission(&self, request: Vec) -> Result; + + /// Answer a feature-support query. `request` is the SCALE-encoded + /// [`HostFeatureSupportedRequest`]. + fn feature_supported(&self, request: Vec) -> Result; + + /// Read a value from the host's scoped key-value store. + fn local_storage_read(&self, key: String) -> Result>, HostStorageError>; + /// Write a value to the host's scoped key-value store. + fn local_storage_write(&self, key: String, value: Vec) -> Result<(), HostStorageError>; + /// Clear a value from the host's scoped key-value store. + fn local_storage_clear(&self, key: String) -> Result<(), HostStorageError>; +} + +/// UniFFI object exposing the TrUAPI core to native hosts. +#[derive(uniffi::Object)] +pub struct NativeTrUApiCore { + core: Arc, + callbacks: Arc, + #[cfg(feature = "ws-bridge")] + bridge: std::sync::Mutex>, +} + +#[uniffi::export] +impl NativeTrUApiCore { + /// Construct the core from a callback object. The native shell hands + /// over its [`HostCallbacks`] trait object; the core wraps it in a + /// [`CallbackPlatform`] and feeds the result into + /// [`TrUApiCore::from_platform`]. + #[uniffi::constructor] + pub fn new(callbacks: Box) -> Arc { + let callbacks: Arc = callbacks.into(); + callbacks.on_core_log( + "truapi.native.core.boot".to_string(), + "core ready".to_string(), + ); + + let platform = Arc::new(CallbackPlatform { + callbacks: callbacks.clone(), + }); + Arc::new(Self { + core: Arc::new(TrUApiCore::from_platform(platform)), + callbacks, + #[cfg(feature = "ws-bridge")] + bridge: std::sync::Mutex::new(None), + }) + } + + /// Push an inbound SCALE-encoded protocol frame from the product into + /// the dispatcher. Responses are emitted back through the + /// [`HostCallbacks::on_core_response`] callback. + pub fn receive_from_product(&self, frame: Vec) -> bool { + self.callbacks.on_core_log( + "truapi.native.core.inbound".to_string(), + format!("frame_bytes={}", frame.len()), + ); + + let callbacks = self.callbacks.clone(); + let core = self.core.clone(); + match catch_unwind(AssertUnwindSafe(|| { + let message = ProtocolMessage::decode(&mut &*frame).ok(); + message.map(|message| { + let transport = Arc::new(NativeCallbackTransport::new(callbacks.clone())); + let transport_dyn: Arc = transport.clone(); + futures::executor::block_on(core.dispatch(message, transport_dyn)); + transport + }) + })) { + Ok(Some(transport)) => { + if transport.sent_count() > 0 { + self.callbacks.on_core_log( + "truapi.native.core.request.ok".to_string(), + format!("response_frames={}", transport.sent_count()), + ); + } else { + self.callbacks.on_core_log( + "truapi.native.core.request.no_response".to_string(), + "dispatcher produced no frame".to_string(), + ); + } + true + } + Ok(None) => { + self.callbacks.on_core_log( + "truapi.native.core.request.decode_failed".to_string(), + "failed to decode inbound frame".to_string(), + ); + false + } + Err(_) => { + self.callbacks.on_core_log( + "truapi.native.core.request.panic".to_string(), + "request handling panicked".to_string(), + ); + false + } + } + } + + /// Push the currently-paired session into the core. Mirrors the JS + /// `setActiveSession`. `pubkey` must be exactly 32 bytes (sr25519 root + /// public key). + pub fn set_active_session( + &self, + pubkey: Vec, + lite_username: Option, + full_username: Option, + ) -> bool { + let Ok(public_key) = <[u8; 32]>::try_from(pubkey.as_slice()) else { + self.callbacks.on_core_log( + "truapi.native.core.session.invalid_pubkey".to_string(), + format!("expected 32 bytes, got {}", pubkey.len()), + ); + return false; + }; + self.core + .session_state() + .set_session(crate::host_logic::session::SessionInfo { + public_key, + lite_username, + full_username, + }); + true + } + + /// Drop the currently-paired session. Mirrors the JS + /// `clearActiveSession`. + pub fn clear_active_session(&self) { + self.core.session_state().clear_session(); + } + + /// Smoke-test helper: return a SCALE-encoded `feature_supported` + /// request frame so the iOS/Android shells can verify the wire path + /// without owning request construction logic. + pub fn debug_smoke_feature_request_frame(&self) -> Vec { + ProtocolMessage { + request_id: "native-smoke:1".to_string(), + payload: Payload { + tag: "system_feature_supported_request".to_string(), + value: HostFeatureSupportedRequest::V1(v01::HostFeatureSupportedRequest::Chain { + genesis_hash: vec![1u8; 32], + }) + .encode(), + }, + } + .encode() + } +} + +#[cfg(feature = "ws-bridge")] +#[uniffi::export] +impl NativeTrUApiCore { + /// Start the localhost WebSocket bridge. Returns the descriptor the + /// host hands to the product so it can dial back in. + pub fn start_ws_bridge(&self, bind_port: u16) -> Result { + let mut guard = self.bridge.lock().unwrap(); + if guard.is_some() { + return Err(WsBridgeStartError::AlreadyRunning); + } + let logger: BridgeLogger = { + let callbacks = self.callbacks.clone(); + Arc::new(move |marker: &str, detail: &str| { + callbacks.on_core_log(marker.to_string(), detail.to_string()); + }) + }; + let (bridge, endpoint) = WsBridge::start(bind_port, self.core.clone(), logger)?; + *guard = Some(bridge); + Ok(endpoint) + } + + /// Stop the localhost WebSocket bridge (if running). + pub fn stop_ws_bridge(&self) { + if let Some(mut bridge) = self.bridge.lock().unwrap().take() { + bridge.stop(); + } + } +} + +struct CallbackPlatform { + callbacks: Arc, +} + +#[async_trait] +impl Navigation for CallbackPlatform { + async fn navigate_to(&self, url: String) -> Result<(), v01::HostNavigateToError> { + self.callbacks.on_core_log( + "truapi.native.callback.navigate_to".to_string(), + url.clone(), + ); + self.callbacks.navigate_to(url).map_err(Into::into) + } +} + +#[async_trait] +impl Notifications for CallbackPlatform { + async fn push_notification( + &self, + notification: v01::HostPushNotificationRequest, + ) -> Result<(), v01::GenericError> { + self.callbacks.on_core_log( + "truapi.native.callback.push_notification".to_string(), + notification.text.clone(), + ); + self.callbacks + .push_notification(notification.encode()) + .map_err(Into::into) + } +} + +#[async_trait] +impl Permissions for CallbackPlatform { + async fn device_permission( + &self, + request: v01::HostDevicePermissionRequest, + ) -> Result { + self.callbacks.on_core_log( + "truapi.native.callback.device_permission".to_string(), + format!("{request}"), + ); + let granted = self + .callbacks + .device_permission(request.encode()) + .map_err(v01::GenericError::from)?; + Ok(v01::HostDevicePermissionResponse { granted }) + } + + async fn remote_permission( + &self, + request: v01::RemotePermissionRequest, + ) -> Result { + self.callbacks.on_core_log( + "truapi.native.callback.remote_permission".to_string(), + format!("{request}"), + ); + let granted = self + .callbacks + .remote_permission(request.encode()) + .map_err(v01::GenericError::from)?; + Ok(v01::RemotePermissionResponse { granted }) + } +} + +#[async_trait] +impl Features for CallbackPlatform { + async fn feature_supported( + &self, + request: HostFeatureSupportedRequest, + ) -> Result { + let supported = self + .callbacks + .feature_supported(request.encode()) + .map_err(v01::GenericError::from)?; + Ok(HostFeatureSupportedResponse::V1( + v01::HostFeatureSupportedResponse { supported }, + )) + } +} + +#[async_trait] +impl Storage for CallbackPlatform { + async fn read(&self, key: String) -> Result>, v01::HostLocalStorageReadError> { + self.callbacks.local_storage_read(key).map_err(Into::into) + } + + async fn write( + &self, + key: String, + value: Vec, + ) -> Result<(), v01::HostLocalStorageReadError> { + self.callbacks + .local_storage_write(key, value) + .map_err(Into::into) + } + + async fn clear(&self, key: String) -> Result<(), v01::HostLocalStorageReadError> { + self.callbacks.local_storage_clear(key).map_err(Into::into) + } +} + +// Account/signing/statement-store/preimage/chain capabilities aren't wired +// through the callback surface yet. The platform trait requires impls, so +// we stub them as "unavailable" responses or empty streams. Filling these +// in is a future phase when the corresponding native callbacks land. + +#[async_trait] +impl ChainProvider for CallbackPlatform { + async fn connect( + &self, + _genesis_hash: truapi_platform::GenesisHash, + ) -> Result, v01::GenericError> { + Err(v01::GenericError::GenericError(v01::GenericErr { + reason: "chain provider not yet wired through native callbacks".into(), + })) + } +} + +#[async_trait] +impl Accounts for CallbackPlatform { + async fn host_account_get( + &self, + _request: HostAccountGetRequest, + ) -> Result { + Err(v01::HostAccountGetError::Unknown { + reason: unavailable_reason("host_account_get"), + }) + } + + async fn host_account_get_alias( + &self, + _request: HostAccountGetAliasRequest, + ) -> Result { + Err(v01::HostAccountGetError::Unknown { + reason: unavailable_reason("host_account_get_alias"), + }) + } + + async fn host_account_create_proof( + &self, + _request: HostAccountCreateProofRequest, + ) -> Result { + Err(v01::HostAccountCreateProofError::Unknown { + reason: unavailable_reason("host_account_create_proof"), + }) + } + + async fn host_get_legacy_accounts( + &self, + _request: HostGetLegacyAccountsRequest, + ) -> Result { + Ok(HostGetLegacyAccountsResponse::V1( + v01::HostGetLegacyAccountsResponse { accounts: vec![] }, + )) + } + + async fn host_account_connection_status_subscribe( + &self, + ) -> BoxStream<'static, HostAccountConnectionStatusSubscribeItem> { + Box::pin(stream::empty()) + } + + async fn host_get_user_id( + &self, + _request: HostGetUserIdRequest, + ) -> Result { + Err(v01::HostGetUserIdError::Unknown { + reason: unavailable_reason("host_get_user_id"), + }) + } +} + +#[async_trait] +impl Signing for CallbackPlatform { + async fn host_sign_payload( + &self, + _request: HostSignPayloadRequest, + ) -> Result { + Err(v01::HostSignPayloadError::Unknown { + reason: unavailable_reason("host_sign_payload"), + }) + } + + async fn host_sign_raw( + &self, + _request: HostSignRawRequest, + ) -> Result { + Err(v01::HostSignPayloadError::Unknown { + reason: unavailable_reason("host_sign_raw"), + }) + } +} + +#[async_trait] +impl StatementStore for CallbackPlatform { + async fn remote_statement_store_subscribe( + &self, + _request: RemoteStatementStoreSubscribeRequest, + ) -> BoxStream<'static, RemoteStatementStoreSubscribeItem> { + Box::pin(stream::empty()) + } + + async fn remote_statement_store_submit( + &self, + _request: RemoteStatementStoreSubmitRequest, + ) -> Result<(), v01::GenericError> { + Err(v01::GenericError::GenericError(v01::GenericErr { + reason: unavailable_reason("remote_statement_store_submit"), + })) + } + + async fn remote_statement_store_create_proof( + &self, + _request: RemoteStatementStoreCreateProofRequest, + ) -> Result + { + Err(v01::RemoteStatementStoreCreateProofError::Unknown { + reason: unavailable_reason("remote_statement_store_create_proof"), + }) + } +} + +#[async_trait] +impl Preimage for CallbackPlatform { + async fn remote_preimage_lookup_subscribe( + &self, + _request: RemotePreimageLookupSubscribeRequest, + ) -> BoxStream<'static, RemotePreimageLookupSubscribeItem> { + Box::pin(stream::empty()) + } +} + +fn unavailable_reason(method: &str) -> String { + format!("{method} unavailable on native host (callback not yet wired)") +} + +struct NativeCallbackTransport { + callbacks: Arc, + sent: AtomicUsize, +} + +impl NativeCallbackTransport { + fn new(callbacks: Arc) -> Self { + Self { + callbacks, + sent: AtomicUsize::new(0), + } + } + + fn sent_count(&self) -> usize { + self.sent.load(Ordering::Relaxed) + } +} + +impl Transport for NativeCallbackTransport { + fn send(&self, message: ProtocolMessage) { + self.sent.fetch_add(1, Ordering::Relaxed); + self.callbacks.on_core_response(message.encode()); + } + + fn on_message( + &self, + _handler: Box, + ) -> Box { + Box::new(|| {}) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Mutex; + + /// Capturing callback object: records every outbound response frame + /// and returns deterministic answers on every prompt. Used to verify + /// that the trait is object-safe and that an inbound + /// `feature_supported` request actually round-trips through the + /// dispatcher. + struct CapturingCallbacks { + responses: Arc>>>, + } + + impl HostCallbacks for CapturingCallbacks { + fn on_core_log(&self, _marker: String, _detail: String) {} + fn on_core_response(&self, frame: Vec) { + self.responses.lock().unwrap().push(frame); + } + fn navigate_to(&self, _url: String) -> Result<(), HostNavigateRejection> { + Ok(()) + } + fn push_notification(&self, _payload: Vec) -> Result<(), HostRejection> { + Ok(()) + } + fn device_permission(&self, _request: Vec) -> Result { + Ok(true) + } + fn remote_permission(&self, _request: Vec) -> Result { + Ok(true) + } + fn feature_supported(&self, _request: Vec) -> Result { + Ok(true) + } + fn local_storage_read(&self, _key: String) -> Result>, HostStorageError> { + Ok(None) + } + fn local_storage_write( + &self, + _key: String, + _value: Vec, + ) -> Result<(), HostStorageError> { + Ok(()) + } + fn local_storage_clear(&self, _key: String) -> Result<(), HostStorageError> { + Ok(()) + } + } + + #[test] + fn native_core_round_trips_feature_supported_through_callbacks() { + let responses: Arc>>> = Arc::new(Mutex::new(Vec::new())); + let core = NativeTrUApiCore::new(Box::new(CapturingCallbacks { + responses: responses.clone(), + })); + + let frame = core.debug_smoke_feature_request_frame(); + assert!(core.receive_from_product(frame)); + + let frames = responses.lock().unwrap(); + assert_eq!(frames.len(), 1, "expected one response frame"); + let response = ProtocolMessage::decode(&mut &frames[0][..]).expect("decode response frame"); + assert_eq!(response.request_id, "native-smoke:1"); + // Wire payload: `Result`-shaped: + // [Ok disc=0x00][V1 variant 0x00][supported=1] + assert_eq!(response.payload.value, vec![0x00, 0x00, 0x01]); + } + + #[test] + fn set_active_session_rejects_wrong_size_pubkey() { + struct Noop; + impl HostCallbacks for Noop { + fn on_core_log(&self, _marker: String, _detail: String) {} + fn on_core_response(&self, _frame: Vec) {} + fn navigate_to(&self, _url: String) -> Result<(), HostNavigateRejection> { + Ok(()) + } + fn push_notification(&self, _payload: Vec) -> Result<(), HostRejection> { + Ok(()) + } + fn device_permission(&self, _request: Vec) -> Result { + Ok(false) + } + fn remote_permission(&self, _request: Vec) -> Result { + Ok(false) + } + fn feature_supported(&self, _request: Vec) -> Result { + Ok(false) + } + fn local_storage_read( + &self, + _key: String, + ) -> Result>, HostStorageError> { + Ok(None) + } + fn local_storage_write( + &self, + _key: String, + _value: Vec, + ) -> Result<(), HostStorageError> { + Ok(()) + } + fn local_storage_clear(&self, _key: String) -> Result<(), HostStorageError> { + Ok(()) + } + } + + let core = NativeTrUApiCore::new(Box::new(Noop)); + assert!(!core.set_active_session(vec![0u8; 16], None, None)); + assert!(core.set_active_session(vec![0u8; 32], None, None)); + core.clear_active_session(); + } +} diff --git a/rust/crates/truapi-server/src/runtime.rs b/rust/crates/truapi-server/src/runtime.rs new file mode 100644 index 00000000..1c0abf32 --- /dev/null +++ b/rust/crates/truapi-server/src/runtime.rs @@ -0,0 +1,889 @@ +//! `PlatformRuntimeHost

` adapts a [`truapi_platform::Platform`] into the +//! typed `truapi::api::*` host traits the generated dispatcher routes to. +//! +//! Most methods are straight delegations to the platform; the rest are +//! either stubbed out as `CallError::Unsupported` (because the v0.1 platform +//! surface does not yet cover them) or carry host-agnostic logic owned by +//! the core (e.g. `dotns` URL parsing for `navigate_to`, the permission +//! cache layer). + +use std::sync::Arc; + +use crate::host_logic::dotns::{NavigateDecision, parse_navigate}; +use crate::host_logic::features::feature_supported; +use crate::host_logic::permissions::{Decision, PermissionsService}; +use crate::host_logic::session::SessionState; + +use truapi::api::{ + Account, Chain, Chat, Entropy, JsonRpc, LocalStorage, Payment, Permissions, Preimage, + ResourceAllocation, Signing, StatementStore, System, Theme, +}; +use truapi::v01; +use truapi::versioned::account::{ + HostAccountConnectionStatusSubscribeItem, HostAccountCreateProofError, + HostAccountCreateProofRequest, HostAccountCreateProofResponse, HostAccountGetAliasError, + HostAccountGetAliasRequest, HostAccountGetAliasResponse, HostAccountGetError, + HostAccountGetRequest, HostAccountGetResponse, HostGetLegacyAccountsError, + HostGetLegacyAccountsRequest, HostGetLegacyAccountsResponse, HostGetUserIdError, + HostGetUserIdRequest, HostGetUserIdResponse, HostRequestLoginError, HostRequestLoginRequest, + HostRequestLoginResponse, +}; +use truapi::versioned::chain::{ + RemoteChainHeadBodyError, RemoteChainHeadBodyRequest, RemoteChainHeadBodyResponse, + RemoteChainHeadCallError, RemoteChainHeadCallRequest, RemoteChainHeadCallResponse, + RemoteChainHeadContinueError, RemoteChainHeadContinueRequest, RemoteChainHeadContinueResponse, + RemoteChainHeadFollowItem, RemoteChainHeadFollowRequest, RemoteChainHeadHeaderError, + RemoteChainHeadHeaderRequest, RemoteChainHeadHeaderResponse, RemoteChainHeadStopOperationError, + RemoteChainHeadStopOperationRequest, RemoteChainHeadStopOperationResponse, + RemoteChainHeadStorageError, RemoteChainHeadStorageRequest, RemoteChainHeadStorageResponse, + RemoteChainHeadUnpinError, RemoteChainHeadUnpinRequest, RemoteChainHeadUnpinResponse, + RemoteChainSpecChainNameError, RemoteChainSpecChainNameRequest, + RemoteChainSpecChainNameResponse, RemoteChainSpecGenesisHashError, + RemoteChainSpecGenesisHashRequest, RemoteChainSpecGenesisHashResponse, + RemoteChainSpecPropertiesError, RemoteChainSpecPropertiesRequest, + RemoteChainSpecPropertiesResponse, RemoteChainTransactionBroadcastError, + RemoteChainTransactionBroadcastRequest, RemoteChainTransactionBroadcastResponse, + RemoteChainTransactionStopError, RemoteChainTransactionStopRequest, + RemoteChainTransactionStopResponse, +}; +use truapi::versioned::local_storage::{ + HostLocalStorageClearError, HostLocalStorageClearRequest, HostLocalStorageClearResponse, + HostLocalStorageReadError, HostLocalStorageReadRequest, HostLocalStorageReadResponse, + HostLocalStorageWriteError, HostLocalStorageWriteRequest, HostLocalStorageWriteResponse, +}; +use truapi::versioned::permissions::{ + HostDevicePermissionError, HostDevicePermissionRequest, HostDevicePermissionResponse, + RemotePermissionError, RemotePermissionRequest, RemotePermissionResponse, +}; +use truapi::versioned::preimage::{ + RemotePreimageLookupSubscribeItem, RemotePreimageLookupSubscribeRequest, +}; +use truapi::versioned::signing::{ + HostSignPayloadError, HostSignPayloadRequest, HostSignPayloadResponse, HostSignRawError, + HostSignRawRequest, HostSignRawResponse, +}; +use truapi::versioned::statement_store::{ + RemoteStatementStoreCreateProofError, RemoteStatementStoreCreateProofRequest, + RemoteStatementStoreCreateProofResponse, RemoteStatementStoreSubmitError, + RemoteStatementStoreSubmitRequest, RemoteStatementStoreSubscribeItem, + RemoteStatementStoreSubscribeRequest, +}; +use truapi::versioned::system::{ + HostFeatureSupportedError, HostFeatureSupportedRequest, HostFeatureSupportedResponse, + HostNavigateToError, HostNavigateToRequest, HostNavigateToResponse, HostPushNotificationError, + HostPushNotificationRequest, HostPushNotificationResponse, +}; +use truapi::{CallContext, CallError, Subscription}; +use truapi_platform::{ + Accounts as PlatformAccounts, Navigation as PlatformNavigation, + Notifications as PlatformNotifications, Platform, Preimage as PlatformPreimage, + Signing as PlatformSigning, StatementStore as PlatformStatementStore, + Storage as PlatformStorage, +}; + +/// Adapter that exposes a [`truapi_platform::Platform`] through the +/// `truapi::api::*` trait set the generated dispatcher routes to. +pub struct PlatformRuntimeHost

{ + platform: Arc

, + /// Currently-paired session, pushed by the host through a side channel. + /// Account-management subscriptions read from this in lieu of round-tripping + /// a callback on every connection-status query. + session_state: Arc, +} + +impl

PlatformRuntimeHost

{ + /// Wrap a platform implementation. The runtime takes ownership via `Arc`. + pub fn new(platform: Arc

) -> Self + where + P: Platform + 'static, + { + Self { + platform, + session_state: SessionState::new(), + } + } + + /// Clone of the shared session-state holder. The platform bridge layer + /// (`setActiveSession` / `clearActiveSession`) routes through this handle. + pub fn session_state(&self) -> Arc { + self.session_state.clone() + } +} + +fn unsupported_with_reason(reason: &str) -> CallError { + CallError::HostFailure { + reason: reason.to_string(), + } +} + +// --------------------------------------------------------------------------- +// System +// --------------------------------------------------------------------------- + +impl

System for PlatformRuntimeHost

+where + P: Platform + 'static, +{ + async fn feature_supported( + &self, + _cx: &CallContext, + request: HostFeatureSupportedRequest, + ) -> Result> { + feature_supported(self.platform.as_ref(), request) + .await + .map_err(|err| CallError::Domain(HostFeatureSupportedError::V1(err))) + } + + async fn push_notification( + &self, + _cx: &CallContext, + request: HostPushNotificationRequest, + ) -> Result> { + let HostPushNotificationRequest::V1(inner) = request; + PlatformNotifications::push_notification(self.platform.as_ref(), inner) + .await + .map(|()| HostPushNotificationResponse::V1) + .map_err(|err| CallError::Domain(HostPushNotificationError::V1(err))) + } + + async fn navigate_to( + &self, + _cx: &CallContext, + request: HostNavigateToRequest, + ) -> Result> { + let HostNavigateToRequest::V1(v01::HostNavigateToRequest { url }) = request; + let resolved = match parse_navigate(&url) { + NavigateDecision::Reject { reason } => { + return Err(CallError::Domain(HostNavigateToError::V1( + v01::HostNavigateToError::Unknown { reason }, + ))); + } + decision => decision + .canonical_url() + .expect("only Reject yields no canonical URL"), + }; + PlatformNavigation::navigate_to(self.platform.as_ref(), resolved) + .await + .map(|()| HostNavigateToResponse::V1) + .map_err(|err| CallError::Domain(HostNavigateToError::V1(err))) + } +} + +// --------------------------------------------------------------------------- +// Permissions +// --------------------------------------------------------------------------- + +impl

Permissions for PlatformRuntimeHost

+where + P: Platform + 'static, +{ + async fn request_device_permission( + &self, + _cx: &CallContext, + request: HostDevicePermissionRequest, + ) -> Result> { + let HostDevicePermissionRequest::V1(inner) = request; + let service = PermissionsService::new(self.platform.as_ref(), self.platform.as_ref()); + match service.check_or_prompt_device(inner).await { + Ok(decision) => Ok(HostDevicePermissionResponse::V1( + v01::HostDevicePermissionResponse { + granted: decision == Decision::Granted, + }, + )), + Err(err) => Err(CallError::HostFailure { + reason: format!("permission storage failed: {err:?}"), + }), + } + } + + async fn request_remote_permission( + &self, + _cx: &CallContext, + request: RemotePermissionRequest, + ) -> Result> { + let RemotePermissionRequest::V1(inner) = request; + let service = PermissionsService::new(self.platform.as_ref(), self.platform.as_ref()); + match service.check_or_prompt_remote(inner).await { + Ok(decision) => Ok(RemotePermissionResponse::V1( + v01::RemotePermissionResponse { + granted: decision == Decision::Granted, + }, + )), + Err(err) => Err(CallError::HostFailure { + reason: format!("permission storage failed: {err:?}"), + }), + } + } +} + +// --------------------------------------------------------------------------- +// LocalStorage +// --------------------------------------------------------------------------- + +impl

LocalStorage for PlatformRuntimeHost

+where + P: Platform + 'static, +{ + async fn read( + &self, + _cx: &CallContext, + request: HostLocalStorageReadRequest, + ) -> Result> { + let HostLocalStorageReadRequest::V1(v01::HostLocalStorageReadRequest { key }) = request; + PlatformStorage::read(self.platform.as_ref(), key) + .await + .map(|value| { + HostLocalStorageReadResponse::V1(v01::HostLocalStorageReadResponse { value }) + }) + .map_err(|err| CallError::Domain(HostLocalStorageReadError::V1(err))) + } + + async fn write( + &self, + _cx: &CallContext, + request: HostLocalStorageWriteRequest, + ) -> Result> { + let HostLocalStorageWriteRequest::V1(v01::HostLocalStorageWriteRequest { key, value }) = + request; + PlatformStorage::write(self.platform.as_ref(), key, value) + .await + .map(|()| HostLocalStorageWriteResponse::V1) + .map_err(|err| CallError::Domain(HostLocalStorageWriteError::V1(err))) + } + + async fn clear( + &self, + _cx: &CallContext, + request: HostLocalStorageClearRequest, + ) -> Result> { + let HostLocalStorageClearRequest::V1(v01::HostLocalStorageClearRequest { key }) = request; + PlatformStorage::clear(self.platform.as_ref(), key) + .await + .map(|()| HostLocalStorageClearResponse::V1) + .map_err(|err| CallError::Domain(HostLocalStorageClearError::V1(err))) + } +} + +// --------------------------------------------------------------------------- +// Account +// --------------------------------------------------------------------------- + +impl

Account for PlatformRuntimeHost

+where + P: Platform + 'static, +{ + async fn connection_status_subscribe( + &self, + _cx: &CallContext, + ) -> Subscription { + Subscription::new(self.session_state.subscribe()) + } + + async fn get_account( + &self, + _cx: &CallContext, + request: HostAccountGetRequest, + ) -> Result> { + PlatformAccounts::host_account_get(self.platform.as_ref(), request) + .await + .map_err(|err| CallError::Domain(HostAccountGetError::V1(err))) + } + + async fn get_account_alias( + &self, + _cx: &CallContext, + request: HostAccountGetAliasRequest, + ) -> Result> { + PlatformAccounts::host_account_get_alias(self.platform.as_ref(), request) + .await + .map_err(|err| CallError::Domain(HostAccountGetAliasError::V1(err))) + } + + async fn create_account_proof( + &self, + _cx: &CallContext, + request: HostAccountCreateProofRequest, + ) -> Result> { + PlatformAccounts::host_account_create_proof(self.platform.as_ref(), request) + .await + .map_err(|err| CallError::Domain(HostAccountCreateProofError::V1(err))) + } + + async fn get_legacy_accounts( + &self, + _cx: &CallContext, + request: HostGetLegacyAccountsRequest, + ) -> Result> { + PlatformAccounts::host_get_legacy_accounts(self.platform.as_ref(), request) + .await + .map_err(|err| CallError::Domain(HostGetLegacyAccountsError::V1(err))) + } + + async fn get_user_id( + &self, + _cx: &CallContext, + request: HostGetUserIdRequest, + ) -> Result> { + PlatformAccounts::host_get_user_id(self.platform.as_ref(), request) + .await + .map_err(|err| CallError::Domain(HostGetUserIdError::V1(err))) + } + + async fn request_login( + &self, + _cx: &CallContext, + _request: HostRequestLoginRequest, + ) -> Result> { + Err(unsupported_with_reason( + "request_login is not part of the v0.1 platform surface", + )) + } +} + +// --------------------------------------------------------------------------- +// Signing +// --------------------------------------------------------------------------- + +impl

Signing for PlatformRuntimeHost

+where + P: Platform + 'static, +{ + async fn sign_payload( + &self, + _cx: &CallContext, + request: HostSignPayloadRequest, + ) -> Result> { + PlatformSigning::host_sign_payload(self.platform.as_ref(), request) + .await + .map_err(|err| CallError::Domain(HostSignPayloadError::V1(err))) + } + + async fn sign_raw( + &self, + _cx: &CallContext, + request: HostSignRawRequest, + ) -> Result> { + PlatformSigning::host_sign_raw(self.platform.as_ref(), request) + .await + .map_err(|err| CallError::Domain(HostSignRawError::V1(err))) + } + + // create_transaction, create_transaction_with_legacy_account, + // sign_payload_with_legacy_account, sign_raw_with_legacy_account fall + // back to the trait defaults (Err(CallError::unavailable())). The v0.1 + // platform surface only covers host_sign_payload / host_sign_raw. +} + +// --------------------------------------------------------------------------- +// StatementStore +// --------------------------------------------------------------------------- + +impl

StatementStore for PlatformRuntimeHost

+where + P: Platform + 'static, +{ + async fn subscribe( + &self, + _cx: &CallContext, + request: RemoteStatementStoreSubscribeRequest, + ) -> Subscription { + let stream = PlatformStatementStore::remote_statement_store_subscribe( + self.platform.as_ref(), + request, + ) + .await; + Subscription::new(stream) + } + + async fn submit( + &self, + _cx: &CallContext, + request: RemoteStatementStoreSubmitRequest, + ) -> Result<(), CallError> { + PlatformStatementStore::remote_statement_store_submit(self.platform.as_ref(), request) + .await + .map_err(|err| CallError::Domain(RemoteStatementStoreSubmitError::V1(err))) + } + + async fn create_proof( + &self, + _cx: &CallContext, + request: RemoteStatementStoreCreateProofRequest, + ) -> Result< + RemoteStatementStoreCreateProofResponse, + CallError, + > { + PlatformStatementStore::remote_statement_store_create_proof(self.platform.as_ref(), request) + .await + .map_err(|err| CallError::Domain(RemoteStatementStoreCreateProofError::V1(err))) + } + + // create_proof_authorized falls back to the default. The v0.1 platform + // surface does not expose pre-allocated allowance signing yet. +} + +// --------------------------------------------------------------------------- +// Preimage +// --------------------------------------------------------------------------- + +impl

Preimage for PlatformRuntimeHost

+where + P: Platform + 'static, +{ + async fn lookup_subscribe( + &self, + _cx: &CallContext, + request: RemotePreimageLookupSubscribeRequest, + ) -> Subscription { + let stream = + PlatformPreimage::remote_preimage_lookup_subscribe(self.platform.as_ref(), request) + .await; + Subscription::new(stream) + } + + // submit falls back to the default. The platform surface does not yet + // include preimage submission. +} + +// --------------------------------------------------------------------------- +// Chain +// --------------------------------------------------------------------------- +// +// Every method on the Chain trait is stubbed as Unsupported: the v0.1 +// platform surface exposes only the raw `ChainProvider::connect` JSON-RPC +// bridge. A chainHead state machine lives in a later phase (see Phase 4d). + +impl

Chain for PlatformRuntimeHost

+where + P: Platform + 'static, +{ + async fn follow_head_subscribe( + &self, + _cx: &CallContext, + _request: RemoteChainHeadFollowRequest, + ) -> Subscription { + Subscription::empty() + } + + async fn get_head_header( + &self, + _cx: &CallContext, + _request: RemoteChainHeadHeaderRequest, + ) -> Result> { + Err(unsupported_with_reason( + "chain runtime not yet provided by the platform layer", + )) + } + + async fn get_head_body( + &self, + _cx: &CallContext, + _request: RemoteChainHeadBodyRequest, + ) -> Result> { + Err(unsupported_with_reason( + "chain runtime not yet provided by the platform layer", + )) + } + + async fn get_head_storage( + &self, + _cx: &CallContext, + _request: RemoteChainHeadStorageRequest, + ) -> Result> { + Err(unsupported_with_reason( + "chain runtime not yet provided by the platform layer", + )) + } + + async fn call_head( + &self, + _cx: &CallContext, + _request: RemoteChainHeadCallRequest, + ) -> Result> { + Err(unsupported_with_reason( + "chain runtime not yet provided by the platform layer", + )) + } + + async fn unpin_head( + &self, + _cx: &CallContext, + _request: RemoteChainHeadUnpinRequest, + ) -> Result> { + Err(unsupported_with_reason( + "chain runtime not yet provided by the platform layer", + )) + } + + async fn continue_head( + &self, + _cx: &CallContext, + _request: RemoteChainHeadContinueRequest, + ) -> Result> { + Err(unsupported_with_reason( + "chain runtime not yet provided by the platform layer", + )) + } + + async fn stop_head_operation( + &self, + _cx: &CallContext, + _request: RemoteChainHeadStopOperationRequest, + ) -> Result> + { + Err(unsupported_with_reason( + "chain runtime not yet provided by the platform layer", + )) + } + + async fn get_spec_genesis_hash( + &self, + _cx: &CallContext, + _request: RemoteChainSpecGenesisHashRequest, + ) -> Result> + { + Err(unsupported_with_reason( + "chain runtime not yet provided by the platform layer", + )) + } + + async fn get_spec_chain_name( + &self, + _cx: &CallContext, + _request: RemoteChainSpecChainNameRequest, + ) -> Result> { + Err(unsupported_with_reason( + "chain runtime not yet provided by the platform layer", + )) + } + + async fn get_spec_properties( + &self, + _cx: &CallContext, + _request: RemoteChainSpecPropertiesRequest, + ) -> Result> { + Err(unsupported_with_reason( + "chain runtime not yet provided by the platform layer", + )) + } + + async fn broadcast_transaction( + &self, + _cx: &CallContext, + _request: RemoteChainTransactionBroadcastRequest, + ) -> Result< + RemoteChainTransactionBroadcastResponse, + CallError, + > { + Err(unsupported_with_reason( + "chain runtime not yet provided by the platform layer", + )) + } + + async fn stop_transaction( + &self, + _cx: &CallContext, + _request: RemoteChainTransactionStopRequest, + ) -> Result> + { + Err(unsupported_with_reason( + "chain runtime not yet provided by the platform layer", + )) + } +} + +// --------------------------------------------------------------------------- +// Traits that defer entirely to default "unavailable" trait bodies. +// +// These API surfaces (Chat, JsonRpc, Payment, ResourceAllocation, Entropy, +// Theme) are not part of the v0.1 platform contract, so we leave every +// method at its default `Err(CallError::unavailable())` body and supply +// empty trait impls here. Adding a method later only requires implementing +// the relevant `truapi_platform::*` extension trait. + +impl

Chat for PlatformRuntimeHost

where P: Platform + 'static {} +impl

JsonRpc for PlatformRuntimeHost

where P: Platform + 'static {} +impl

Payment for PlatformRuntimeHost

where P: Platform + 'static {} +impl

ResourceAllocation for PlatformRuntimeHost

where P: Platform + 'static {} +impl

Entropy for PlatformRuntimeHost

where P: Platform + 'static {} +impl

Theme for PlatformRuntimeHost

where P: Platform + 'static {} + +#[cfg(test)] +mod tests { + use super::*; + use async_trait::async_trait; + use futures::stream::{self, BoxStream}; + use parity_scale_codec::Encode; + use truapi::v01; + use truapi_platform::{ + Accounts as PlatformAccounts, ChainProvider, Features as PlatformFeatures, GenesisHash, + JsonRpcConnection, Navigation as PlatformNavigation, + Notifications as PlatformNotifications, Permissions as PlatformPermissions, + Preimage as PlatformPreimage, Signing as PlatformSigning, + StatementStore as PlatformStatementStore, Storage as PlatformStorage, + }; + + /// Minimal Platform impl that only answers `feature_supported`. Every + /// other callback returns a unit value or empty stream, so the runtime + /// can exercise its delegation paths without pulling in a real backend. + struct StubPlatform; + + #[async_trait] + impl PlatformStorage for StubPlatform { + async fn read( + &self, + _key: String, + ) -> Result>, v01::HostLocalStorageReadError> { + Ok(None) + } + async fn write( + &self, + _key: String, + _value: Vec, + ) -> Result<(), v01::HostLocalStorageReadError> { + Ok(()) + } + async fn clear(&self, _key: String) -> Result<(), v01::HostLocalStorageReadError> { + Ok(()) + } + } + + #[async_trait] + impl PlatformNavigation for StubPlatform { + async fn navigate_to(&self, _url: String) -> Result<(), v01::HostNavigateToError> { + Ok(()) + } + } + + #[async_trait] + impl PlatformNotifications for StubPlatform { + async fn push_notification( + &self, + _notification: v01::HostPushNotificationRequest, + ) -> Result<(), v01::GenericError> { + Ok(()) + } + } + + #[async_trait] + impl PlatformPermissions for StubPlatform { + async fn device_permission( + &self, + _request: v01::HostDevicePermissionRequest, + ) -> Result { + Ok(v01::HostDevicePermissionResponse { granted: true }) + } + + async fn remote_permission( + &self, + _request: v01::RemotePermissionRequest, + ) -> Result { + Ok(v01::RemotePermissionResponse { granted: true }) + } + } + + #[async_trait] + impl PlatformFeatures for StubPlatform { + async fn feature_supported( + &self, + request: HostFeatureSupportedRequest, + ) -> Result { + let HostFeatureSupportedRequest::V1(_) = request; + Ok(HostFeatureSupportedResponse::V1( + v01::HostFeatureSupportedResponse { supported: true }, + )) + } + } + + struct DeadConnection; + impl JsonRpcConnection for DeadConnection { + fn send(&self, _request: String) {} + fn responses(&self) -> BoxStream<'static, String> { + Box::pin(stream::empty()) + } + } + + #[async_trait] + impl ChainProvider for StubPlatform { + async fn connect( + &self, + _genesis_hash: GenesisHash, + ) -> Result, v01::GenericError> { + Ok(Box::new(DeadConnection)) + } + } + + #[async_trait] + impl PlatformAccounts for StubPlatform { + async fn host_account_get( + &self, + _request: truapi::versioned::account::HostAccountGetRequest, + ) -> Result + { + Err(v01::HostAccountGetError::NotConnected) + } + async fn host_account_get_alias( + &self, + _request: truapi::versioned::account::HostAccountGetAliasRequest, + ) -> Result + { + Err(v01::HostAccountGetError::NotConnected) + } + async fn host_account_create_proof( + &self, + _request: truapi::versioned::account::HostAccountCreateProofRequest, + ) -> Result< + truapi::versioned::account::HostAccountCreateProofResponse, + v01::HostAccountCreateProofError, + > { + Err(v01::HostAccountCreateProofError::RingNotFound) + } + async fn host_get_legacy_accounts( + &self, + _request: truapi::versioned::account::HostGetLegacyAccountsRequest, + ) -> Result< + truapi::versioned::account::HostGetLegacyAccountsResponse, + v01::HostAccountGetError, + > { + Ok( + truapi::versioned::account::HostGetLegacyAccountsResponse::V1( + v01::HostGetLegacyAccountsResponse { accounts: vec![] }, + ), + ) + } + async fn host_account_connection_status_subscribe( + &self, + ) -> BoxStream<'static, HostAccountConnectionStatusSubscribeItem> { + Box::pin(stream::empty()) + } + async fn host_get_user_id( + &self, + _request: truapi::versioned::account::HostGetUserIdRequest, + ) -> Result + { + Err(v01::HostGetUserIdError::NotConnected) + } + } + + #[async_trait] + impl PlatformSigning for StubPlatform { + async fn host_sign_payload( + &self, + _request: HostSignPayloadRequest, + ) -> Result { + Err(v01::HostSignPayloadError::Rejected) + } + async fn host_sign_raw( + &self, + _request: HostSignRawRequest, + ) -> Result { + Err(v01::HostSignPayloadError::Rejected) + } + } + + #[async_trait] + impl PlatformStatementStore for StubPlatform { + async fn remote_statement_store_subscribe( + &self, + _request: RemoteStatementStoreSubscribeRequest, + ) -> BoxStream<'static, RemoteStatementStoreSubscribeItem> { + Box::pin(stream::empty()) + } + async fn remote_statement_store_submit( + &self, + _request: RemoteStatementStoreSubmitRequest, + ) -> Result<(), v01::GenericError> { + Ok(()) + } + async fn remote_statement_store_create_proof( + &self, + _request: RemoteStatementStoreCreateProofRequest, + ) -> Result< + RemoteStatementStoreCreateProofResponse, + v01::RemoteStatementStoreCreateProofError, + > { + Err(v01::RemoteStatementStoreCreateProofError::UnableToSign) + } + } + + #[async_trait] + impl PlatformPreimage for StubPlatform { + async fn remote_preimage_lookup_subscribe( + &self, + _request: RemotePreimageLookupSubscribeRequest, + ) -> BoxStream<'static, RemotePreimageLookupSubscribeItem> { + Box::pin(stream::empty()) + } + } + + #[test] + fn feature_supported_round_trips_through_runtime() { + let host = PlatformRuntimeHost::new(Arc::new(StubPlatform)); + let cx = CallContext::new(); + let request = HostFeatureSupportedRequest::V1(v01::HostFeatureSupportedRequest::Chain { + genesis_hash: vec![0u8; 32], + }); + let response = futures::executor::block_on(host.feature_supported(&cx, request)).unwrap(); + let HostFeatureSupportedResponse::V1(inner) = response; + assert!(inner.supported); + } + + #[test] + fn navigate_to_uses_dotns_decision_and_then_platform() { + let host = PlatformRuntimeHost::new(Arc::new(StubPlatform)); + let cx = CallContext::new(); + let request = HostNavigateToRequest::V1(v01::HostNavigateToRequest { + url: "mytestapp.dot".to_string(), + }); + let response = futures::executor::block_on(host.navigate_to(&cx, request)).unwrap(); + assert_eq!(response, HostNavigateToResponse::V1); + } + + #[test] + fn navigate_to_rejects_empty_input_without_calling_platform() { + let host = PlatformRuntimeHost::new(Arc::new(StubPlatform)); + let cx = CallContext::new(); + let request = HostNavigateToRequest::V1(v01::HostNavigateToRequest { + url: "".to_string(), + }); + let err = futures::executor::block_on(host.navigate_to(&cx, request)).unwrap_err(); + match err { + CallError::Domain(HostNavigateToError::V1(v01::HostNavigateToError::Unknown { + .. + })) => {} + other => panic!("expected Unknown navigate error, got {other:?}"), + } + } + + #[test] + fn request_login_returns_unsupported() { + let host = PlatformRuntimeHost::new(Arc::new(StubPlatform)); + let cx = CallContext::new(); + let request = HostRequestLoginRequest::V1(v01::HostRequestLoginRequest { reason: None }); + let err = futures::executor::block_on(host.request_login(&cx, request)).unwrap_err(); + assert!(matches!(err, CallError::HostFailure { .. })); + } + + #[test] + fn permissions_grants_and_caches() { + let host = PlatformRuntimeHost::new(Arc::new(StubPlatform)); + let cx = CallContext::new(); + let request = HostDevicePermissionRequest::V1(v01::HostDevicePermissionRequest::Camera); + let response = + futures::executor::block_on(host.request_device_permission(&cx, request)).unwrap(); + let HostDevicePermissionResponse::V1(inner) = response; + assert!(inner.granted); + } + + #[test] + fn feature_supported_encodes_response_to_known_bytes() { + let host = PlatformRuntimeHost::new(Arc::new(StubPlatform)); + let cx = CallContext::new(); + let request = HostFeatureSupportedRequest::V1(v01::HostFeatureSupportedRequest::Chain { + genesis_hash: vec![0u8; 32], + }); + let response = futures::executor::block_on(host.feature_supported(&cx, request)).unwrap(); + // [V1 variant=0][supported=1] + assert_eq!(response.encode(), vec![0x00, 0x01]); + } +} diff --git a/rust/crates/truapi-server/src/subscription.rs b/rust/crates/truapi-server/src/subscription.rs new file mode 100644 index 00000000..6b5bf9a4 --- /dev/null +++ b/rust/crates/truapi-server/src/subscription.rs @@ -0,0 +1,297 @@ +//! Subscription lifecycle management. +//! +//! Tracks active subscriptions (start/receive/stop/interrupt) and handles +//! cleanup when either side terminates. + +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +use futures::StreamExt; +use futures::future::{Either, select}; +use futures::stream::BoxStream; +use parity_scale_codec::Encode; + +use crate::frame::{FrameKind, Payload, ProtocolMessage, compose_action}; +use crate::transport::Transport; + +type StopFn = Box; + +fn spawn_subscription(future: F) +where + F: std::future::Future + Send + 'static, +{ + std::thread::spawn(move || { + futures::executor::block_on(future); + }); +} + +/// One yielded value of a subscription stream after SCALE-encoding. +pub enum SubscriptionOutput { + /// A regular subscription item to deliver as a `_receive` frame. + Item(Vec), + /// Stream-initiated termination delivered as an `_interrupt` frame. + Interrupt(Vec), +} + +/// Boxed stream of [`SubscriptionOutput`] consumed by the dispatcher. +pub type SubscriptionStream = BoxStream<'static, SubscriptionOutput>; + +/// Wrap a host-side stream of typed items into the SCALE-encoded +/// [`SubscriptionStream`] that the dispatcher delivers to the transport. +/// +/// `Item` is the versioned wrapper for each emitted value (e.g. +/// `versioned::account::HostAccountConnectionStatusSubscribeItem`). The +/// generated dispatcher calls this with the second type parameter inferred +/// from the host trait return. +pub fn subscription_stream(stream: S) -> SubscriptionStream +where + Item: Encode + 'static, + S: futures::Stream + Send + 'static, +{ + Box::pin(stream.map(|item| SubscriptionOutput::Item(item.encode()))) +} + +/// Manages active subscriptions on the server side. +pub struct SubscriptionManager { + active: Arc>>, +} + +impl SubscriptionManager { + /// Create an empty manager. + pub fn new() -> Self { + Self { + active: Arc::new(Mutex::new(HashMap::new())), + } + } + + /// Register a subscription: forward stream items as `_receive` frames. + /// Returns when the stream ends or `_stop` is received. + pub fn register( + &self, + request_id: String, + method: &str, + mut stream: SubscriptionStream, + transport: Arc, + ) { + let action = compose_action(method, FrameKind::Receive); + let interrupt_action = compose_action(method, FrameKind::Interrupt); + let completed_interrupt_action = interrupt_action.clone(); + let rid = request_id.clone(); + let stream_transport = transport.clone(); + + // Cancellation channel. + let (cancel_tx, cancel_rx) = futures::channel::oneshot::channel::<()>(); + + // Store the cancel handle. + { + let mut active = self.active.lock().unwrap(); + active.insert( + request_id.clone(), + Box::new(move || { + let _ = cancel_tx.send(()); + }), + ); + } + + let active = self.active.clone(); + + spawn_subscription(async move { + let completed = { + let mut cancel_rx = cancel_rx; + loop { + match select(cancel_rx, stream.next()).await { + Either::Left((_cancelled, _next)) => break false, + Either::Right((item, next_cancel_rx)) => { + cancel_rx = next_cancel_rx; + match item { + Some(SubscriptionOutput::Item(value)) => { + stream_transport.send(ProtocolMessage { + request_id: rid.clone(), + payload: Payload { + tag: action.clone(), + value, + }, + }) + } + Some(SubscriptionOutput::Interrupt(value)) => { + stream_transport.send(ProtocolMessage { + request_id: rid.clone(), + payload: Payload { + tag: interrupt_action.clone(), + value, + }, + }); + break false; + } + None => break true, + } + } + } + } + }; + + let removed = { + let mut active = active.lock().unwrap(); + active.remove(&request_id).is_some() + }; + + if completed && removed { + transport.send(ProtocolMessage { + request_id, + payload: Payload { + tag: completed_interrupt_action, + value: Vec::new(), + }, + }); + } + }); + } + + /// Handle a `_stop` frame from the product side. + pub fn handle_stop(&self, request_id: &str) { + let mut active = self.active.lock().unwrap(); + if let Some(cancel) = active.remove(request_id) { + cancel(); + } + } + + /// Send an `_interrupt` frame to the product side. + pub fn interrupt(&self, request_id: &str, method: &str, transport: &dyn Transport) { + let mut active = self.active.lock().unwrap(); + active.remove(request_id); + let msg = ProtocolMessage { + request_id: request_id.to_string(), + payload: Payload { + tag: compose_action(method, FrameKind::Interrupt), + value: Vec::new(), + }, + }; + transport.send(msg); + } +} + +impl Default for SubscriptionManager { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use futures::stream; + + /// Transport that records every frame and notifies waiters when it + /// reaches a target count. Used to wait for the subscription's + /// background thread to drain a known number of frames. + struct RecordingTransport { + sent: Mutex>, + cvar: std::sync::Condvar, + } + + impl RecordingTransport { + fn new() -> Self { + Self { + sent: Mutex::new(Vec::new()), + cvar: std::sync::Condvar::new(), + } + } + fn sent(&self) -> Vec { + self.sent.lock().unwrap().clone() + } + /// Wait until at least `count` frames have been recorded, or + /// `timeout` elapses. Returns the number of frames recorded at + /// wake-up time. + fn wait_for(&self, count: usize, timeout: std::time::Duration) -> usize { + let mut guard = self.sent.lock().unwrap(); + let deadline = std::time::Instant::now() + timeout; + while guard.len() < count { + let now = std::time::Instant::now(); + if now >= deadline { + break; + } + let (new_guard, _) = self.cvar.wait_timeout(guard, deadline - now).unwrap(); + guard = new_guard; + } + guard.len() + } + } + + impl Transport for RecordingTransport { + fn send(&self, message: ProtocolMessage) { + self.sent.lock().unwrap().push(message); + self.cvar.notify_all(); + } + fn on_message( + &self, + _handler: Box, + ) -> Box { + Box::new(|| {}) + } + } + + fn dummy_stream(items: Vec>) -> SubscriptionStream { + Box::pin(stream::iter( + items.into_iter().map(SubscriptionOutput::Item), + )) + } + + /// Register a never-ending stream then immediately stop it. The + /// stream's first poll must observe cancellation and exit without + /// having pushed any frame. + #[test] + fn register_then_stop_emits_no_extra_frames() { + let transport_typed = Arc::new(RecordingTransport::new()); + let transport_dyn: Arc = transport_typed.clone(); + let manager = SubscriptionManager::new(); + let slow_stream: SubscriptionStream = Box::pin(stream::pending()); + manager.register("p:1".to_string(), "demo_method", slow_stream, transport_dyn); + manager.handle_stop("p:1"); + // Give the worker thread a beat to observe the cancel. + std::thread::sleep(std::time::Duration::from_millis(50)); + assert!( + transport_typed.sent().is_empty(), + "stopped subscription must not push any frame" + ); + } + + /// A stream that yields 2 items then ends naturally must produce 2 + /// `_receive` frames followed by one `_interrupt` frame. + #[test] + fn register_completion_emits_interrupt() { + let transport_typed = Arc::new(RecordingTransport::new()); + let transport_dyn: Arc = transport_typed.clone(); + let manager = SubscriptionManager::new(); + let items = dummy_stream(vec![vec![0xaa], vec![0xbb]]); + manager.register("p:1".to_string(), "demo_method", items, transport_dyn); + let observed = transport_typed.wait_for(3, std::time::Duration::from_secs(2)); + assert_eq!(observed, 3, "expected 2 receive frames + 1 interrupt"); + let frames = transport_typed.sent(); + assert_eq!(frames[0].payload.tag, "demo_method_receive"); + assert_eq!(frames[0].payload.value, vec![0xaa]); + assert_eq!(frames[1].payload.tag, "demo_method_receive"); + assert_eq!(frames[1].payload.value, vec![0xbb]); + assert_eq!(frames[2].payload.tag, "demo_method_interrupt"); + assert_eq!(frames[2].payload.value, Vec::::new()); + } + + /// Calling `handle_stop` twice on the same request id must be a + /// no-op the second time around (the entry has already been removed, + /// no panic, no extra frames). + #[test] + fn double_stop_is_idempotent() { + let transport_typed = Arc::new(RecordingTransport::new()); + let transport_dyn: Arc = transport_typed.clone(); + let manager = SubscriptionManager::new(); + let slow_stream: SubscriptionStream = Box::pin(stream::pending()); + manager.register("p:1".to_string(), "demo_method", slow_stream, transport_dyn); + manager.handle_stop("p:1"); + // Second call must not panic and must not emit any frame. + manager.handle_stop("p:1"); + std::thread::sleep(std::time::Duration::from_millis(50)); + assert!( + transport_typed.sent().is_empty(), + "double-stop must not emit any frame" + ); + } +} diff --git a/rust/crates/truapi-server/src/transport.rs b/rust/crates/truapi-server/src/transport.rs new file mode 100644 index 00000000..ba58481f --- /dev/null +++ b/rust/crates/truapi-server/src/transport.rs @@ -0,0 +1,12 @@ +//! Transport abstraction over platform-specific IPC mechanisms. + +use crate::frame::ProtocolMessage; + +/// A raw message pipe. Platform-specific implementations provide this. +pub trait Transport: Send + Sync { + /// Send a protocol message to the other side. + fn send(&self, message: ProtocolMessage); + + /// Register a handler for incoming messages. Returns an unsubscribe handle. + fn on_message(&self, handler: Box) -> Box; +} diff --git a/rust/crates/truapi-server/src/wasm.rs b/rust/crates/truapi-server/src/wasm.rs new file mode 100644 index 00000000..d2494365 --- /dev/null +++ b/rust/crates/truapi-server/src/wasm.rs @@ -0,0 +1,799 @@ +//! wasm-bindgen surface. Exposes [`WasmTrUApiCore`] to JavaScript hosts so +//! they can wire the TrUAPI core into a browser or worker shell. +//! +//! The browser side hands a `callbacks` object (a `JsBridge`) to the +//! constructor. The bridge implements every host-side capability the +//! [`truapi_platform::Platform`] trait set requires. Internally the bridge +//! is wrapped in a [`SendWrapper`] so it satisfies the `Send` bound that +//! `async_trait`-generated futures inherit — sound on wasm32 because the +//! runtime is single-threaded. +//! +//! Adaptation note: the JS bridge exposes `devicePermission` and +//! `remotePermission` as separate callbacks (matching the v0.1 split) and +//! retains `featureSupported` + `navigateTo`. + +use std::cell::Cell; +use std::rc::Rc; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; + +use async_trait::async_trait; +use futures::channel::mpsc; +use futures::stream::{BoxStream, StreamExt}; +use js_sys::{Function, Reflect, Uint8Array}; +use parity_scale_codec::{Decode, Encode}; +use send_wrapper::SendWrapper; +use truapi::v01; +use truapi::versioned::account::{ + HostAccountConnectionStatusSubscribeItem, HostAccountCreateProofRequest, + HostAccountCreateProofResponse, HostAccountGetAliasRequest, HostAccountGetAliasResponse, + HostAccountGetRequest, HostAccountGetResponse, HostGetLegacyAccountsRequest, + HostGetLegacyAccountsResponse, HostGetUserIdRequest, HostGetUserIdResponse, +}; +use truapi::versioned::preimage::{ + RemotePreimageLookupSubscribeItem, RemotePreimageLookupSubscribeRequest, +}; +use truapi::versioned::signing::{ + HostSignPayloadRequest, HostSignPayloadResponse, HostSignRawRequest, HostSignRawResponse, +}; +use truapi::versioned::statement_store::{ + RemoteStatementStoreCreateProofRequest, RemoteStatementStoreCreateProofResponse, + RemoteStatementStoreSubmitRequest, RemoteStatementStoreSubscribeItem, + RemoteStatementStoreSubscribeRequest, +}; +use truapi::versioned::system::{HostFeatureSupportedRequest, HostFeatureSupportedResponse}; +use truapi_platform::{ + Accounts, ChainProvider, Features, GenesisHash, JsonRpcConnection, Navigation, Notifications, + Permissions, Preimage, Signing, StatementStore, Storage, +}; +use wasm_bindgen::JsCast; +use wasm_bindgen::prelude::*; + +use crate::TrUApiCore; +use crate::frame::ProtocolMessage; +use crate::transport::Transport; + +/// Bundle of JS-side callbacks the bridge invokes. Names map to camelCase +/// keys on the JS object passed to the constructor. +struct JsBridge { + navigate_to: Function, + push_notification: Function, + device_permission: Function, + remote_permission: Function, + feature_supported: Function, + local_storage_read: Function, + local_storage_write: Function, + local_storage_clear: Function, + account_get: Function, + account_get_alias: Function, + account_create_proof: Function, + get_legacy_accounts: Function, + account_connection_status_subscribe: Function, + get_user_id: Function, + sign_payload: Function, + sign_raw: Function, + statement_store_subscribe: Function, + statement_store_submit: Function, + statement_store_create_proof: Function, + preimage_lookup_subscribe: Function, + /// Optional. Hosts that own JSON-RPC connections (e.g. dotli with its + /// "smoldot vs RPC node" toggle) provide this; otherwise chain calls + /// fail with an "unavailable" reason. + chain_connect: Option, + emit_frame: Function, + dispose: Function, +} + +impl JsBridge { + fn from_js(callbacks: &JsValue) -> Result { + Ok(Self { + navigate_to: get_function(callbacks, "navigateTo")?, + push_notification: get_function(callbacks, "pushNotification")?, + device_permission: get_function(callbacks, "devicePermission")?, + remote_permission: get_function(callbacks, "remotePermission")?, + feature_supported: get_function(callbacks, "featureSupported")?, + local_storage_read: get_function(callbacks, "localStorageRead")?, + local_storage_write: get_function(callbacks, "localStorageWrite")?, + local_storage_clear: get_function(callbacks, "localStorageClear")?, + account_get: get_function(callbacks, "accountGet")?, + account_get_alias: get_function(callbacks, "accountGetAlias")?, + account_create_proof: get_function(callbacks, "accountCreateProof")?, + get_legacy_accounts: get_function(callbacks, "getLegacyAccounts")?, + account_connection_status_subscribe: get_function( + callbacks, + "accountConnectionStatusSubscribe", + )?, + get_user_id: get_function(callbacks, "getUserId")?, + sign_payload: get_function(callbacks, "signPayload")?, + sign_raw: get_function(callbacks, "signRaw")?, + statement_store_subscribe: get_function(callbacks, "statementStoreSubscribe")?, + statement_store_submit: get_function(callbacks, "statementStoreSubmit")?, + statement_store_create_proof: get_function(callbacks, "statementStoreCreateProof")?, + preimage_lookup_subscribe: get_function(callbacks, "preimageLookupSubscribe")?, + chain_connect: get_optional_function(callbacks, "chainConnect")?, + emit_frame: get_function(callbacks, "emitFrame")?, + dispose: get_optional_function(callbacks, "dispose")?.unwrap_or_else(noop_function), + }) + } +} + +struct WasmCallbackTransport { + bridge: SendWrapper>, + disposed: Arc, +} + +impl Transport for WasmCallbackTransport { + fn send(&self, message: ProtocolMessage) { + if self.disposed.load(Ordering::Relaxed) { + return; + } + let frame = Uint8Array::from(message.encode().as_slice()); + if let Err(err) = self.bridge.emit_frame.call1(&JsValue::NULL, &frame) { + web_sys::console::error_1(&err); + } + } + + fn on_message( + &self, + _handler: Box, + ) -> Box { + Box::new(|| {}) + } +} + +struct WasmPlatform { + bridge: SendWrapper>, +} + +impl WasmPlatform { + fn new(bridge: Arc) -> Self { + Self { + bridge: SendWrapper::new(bridge), + } + } +} + +#[async_trait] +impl Navigation for WasmPlatform { + async fn navigate_to(&self, url: String) -> Result<(), v01::HostNavigateToError> { + invoke_navigate_to(&self.bridge, &url) + .await + .map_err(|reason| v01::HostNavigateToError::Unknown { reason }) + } +} + +#[async_trait] +impl Notifications for WasmPlatform { + async fn push_notification( + &self, + notification: v01::HostPushNotificationRequest, + ) -> Result<(), v01::GenericError> { + invoke_unit(&self.bridge.push_notification, notification.encode()) + .await + .map_err(generic) + } +} + +#[async_trait] +impl Permissions for WasmPlatform { + async fn device_permission( + &self, + request: v01::HostDevicePermissionRequest, + ) -> Result { + let granted = invoke_bool(&self.bridge.device_permission, request.encode()) + .await + .map_err(generic)?; + Ok(v01::HostDevicePermissionResponse { granted }) + } + + async fn remote_permission( + &self, + request: v01::RemotePermissionRequest, + ) -> Result { + let granted = invoke_bool(&self.bridge.remote_permission, request.encode()) + .await + .map_err(generic)?; + Ok(v01::RemotePermissionResponse { granted }) + } +} + +#[async_trait] +impl Features for WasmPlatform { + async fn feature_supported( + &self, + request: HostFeatureSupportedRequest, + ) -> Result { + let supported = invoke_bool(&self.bridge.feature_supported, request.encode()) + .await + .map_err(generic)?; + Ok(HostFeatureSupportedResponse::V1( + v01::HostFeatureSupportedResponse { supported }, + )) + } +} + +#[async_trait] +impl Storage for WasmPlatform { + async fn read(&self, key: String) -> Result>, v01::HostLocalStorageReadError> { + invoke_local_storage_read(&self.bridge, &key) + .await + .map_err(|reason| v01::HostLocalStorageReadError::Unknown { reason }) + } + + async fn write( + &self, + key: String, + value: Vec, + ) -> Result<(), v01::HostLocalStorageReadError> { + invoke_local_storage_write(&self.bridge, &key, &value) + .await + .map_err(|reason| v01::HostLocalStorageReadError::Unknown { reason }) + } + + async fn clear(&self, key: String) -> Result<(), v01::HostLocalStorageReadError> { + invoke_local_storage_clear(&self.bridge, &key) + .await + .map_err(|reason| v01::HostLocalStorageReadError::Unknown { reason }) + } +} + +#[async_trait] +impl ChainProvider for WasmPlatform { + async fn connect( + &self, + genesis_hash: GenesisHash, + ) -> Result, v01::GenericError> { + let chain_connect = match self.bridge.chain_connect.clone() { + Some(f) => f, + None => { + return Err(generic( + "chainConnect callback not provided by host".to_string(), + )); + } + }; + let chain_connect = SendWrapper::new(chain_connect); + SendWrapper::new(async move { + let (response_tx, response_rx) = mpsc::unbounded::(); + let on_response = Closure::wrap(Box::new(move |json: JsValue| { + let s = json.as_string().unwrap_or_default(); + let _ = response_tx.unbounded_send(s); + }) as Box); + + let genesis_hex = genesis_hash.iter().fold( + String::with_capacity(2 + genesis_hash.len() * 2), + |mut s, b| { + use std::fmt::Write; + let _ = write!(s, "{b:02x}"); + s + }, + ); + let genesis_arg = JsValue::from_str(&format!("0x{genesis_hex}")); + let returned = chain_connect + .call2( + &JsValue::NULL, + &genesis_arg, + on_response.as_ref().unchecked_ref(), + ) + .map_err(|err| generic(js_to_string(err)))?; + let resolved = await_optional_promise(returned).await.map_err(generic)?; + if resolved.is_null() || resolved.is_undefined() { + return Err(generic("chainConnect returned no connection".into())); + } + let send_fn = Reflect::get(&resolved, &JsValue::from_str("send")) + .map_err(|_| generic("chainConnect must return { send, close }".into()))? + .dyn_into::() + .map_err(|_| generic("chainConnect.send must be a function".into()))?; + let close_fn = Reflect::get(&resolved, &JsValue::from_str("close")) + .map_err(|_| generic("chainConnect.close must be a function".into()))? + .dyn_into::() + .map_err(|_| generic("chainConnect.close must be a function".into()))?; + + Ok(Box::new(JsCallbackJsonRpcConnection { + send_fn: SendWrapper::new(send_fn), + close_fn: SendWrapper::new(close_fn), + _on_response: SendWrapper::new(on_response), + response_rx: std::sync::Mutex::new(Some(response_rx)), + }) as Box) + }) + .await + } +} + +#[async_trait] +impl Accounts for WasmPlatform { + async fn host_account_get( + &self, + request: HostAccountGetRequest, + ) -> Result { + invoke_request_response(&self.bridge.account_get, request.encode()) + .await + .map_err(|reason| v01::HostAccountGetError::Unknown { reason }) + } + + async fn host_account_get_alias( + &self, + request: HostAccountGetAliasRequest, + ) -> Result { + invoke_request_response(&self.bridge.account_get_alias, request.encode()) + .await + .map_err(|reason| v01::HostAccountGetError::Unknown { reason }) + } + + async fn host_account_create_proof( + &self, + request: HostAccountCreateProofRequest, + ) -> Result { + invoke_request_response(&self.bridge.account_create_proof, request.encode()) + .await + .map_err(|reason| v01::HostAccountCreateProofError::Unknown { reason }) + } + + async fn host_get_legacy_accounts( + &self, + request: HostGetLegacyAccountsRequest, + ) -> Result { + invoke_request_response(&self.bridge.get_legacy_accounts, request.encode()) + .await + .map_err(|reason| v01::HostAccountGetError::Unknown { reason }) + } + + async fn host_account_connection_status_subscribe( + &self, + ) -> BoxStream<'static, HostAccountConnectionStatusSubscribeItem> { + invoke_subscription(&self.bridge.account_connection_status_subscribe, None) + } + + async fn host_get_user_id( + &self, + request: HostGetUserIdRequest, + ) -> Result { + invoke_request_response(&self.bridge.get_user_id, request.encode()) + .await + .map_err(|reason| v01::HostGetUserIdError::Unknown { reason }) + } +} + +#[async_trait] +impl Signing for WasmPlatform { + async fn host_sign_payload( + &self, + request: HostSignPayloadRequest, + ) -> Result { + invoke_request_response(&self.bridge.sign_payload, request.encode()) + .await + .map_err(|reason| v01::HostSignPayloadError::Unknown { reason }) + } + + async fn host_sign_raw( + &self, + request: HostSignRawRequest, + ) -> Result { + invoke_request_response(&self.bridge.sign_raw, request.encode()) + .await + .map_err(|reason| v01::HostSignPayloadError::Unknown { reason }) + } +} + +#[async_trait] +impl StatementStore for WasmPlatform { + async fn remote_statement_store_subscribe( + &self, + request: RemoteStatementStoreSubscribeRequest, + ) -> BoxStream<'static, RemoteStatementStoreSubscribeItem> { + invoke_subscription( + &self.bridge.statement_store_subscribe, + Some(request.encode()), + ) + } + + async fn remote_statement_store_submit( + &self, + request: RemoteStatementStoreSubmitRequest, + ) -> Result<(), v01::GenericError> { + invoke_unit(&self.bridge.statement_store_submit, request.encode()) + .await + .map_err(generic) + } + + async fn remote_statement_store_create_proof( + &self, + request: RemoteStatementStoreCreateProofRequest, + ) -> Result + { + invoke_request_response(&self.bridge.statement_store_create_proof, request.encode()) + .await + .map_err(|reason| v01::RemoteStatementStoreCreateProofError::Unknown { reason }) + } +} + +#[async_trait] +impl Preimage for WasmPlatform { + async fn remote_preimage_lookup_subscribe( + &self, + request: RemotePreimageLookupSubscribeRequest, + ) -> BoxStream<'static, RemotePreimageLookupSubscribeItem> { + invoke_subscription( + &self.bridge.preimage_lookup_subscribe, + Some(request.encode()), + ) + } +} + +struct JsCallbackJsonRpcConnection { + send_fn: SendWrapper, + close_fn: SendWrapper, + /// Closure must outlive the connection so JS keeps a live ref to the + /// response sink. Dropped together with the rest of the struct. + _on_response: SendWrapper>, + response_rx: std::sync::Mutex>>, +} + +impl JsonRpcConnection for JsCallbackJsonRpcConnection { + fn send(&self, request: String) { + let arg = JsValue::from_str(&request); + if let Err(err) = self.send_fn.call1(&JsValue::NULL, &arg) { + web_sys::console::error_1(&err); + } + } + + fn responses(&self) -> BoxStream<'static, String> { + let mut guard = self.response_rx.lock().unwrap(); + match guard.take() { + Some(rx) => rx.boxed(), + None => futures::stream::empty().boxed(), + } + } +} + +impl Drop for JsCallbackJsonRpcConnection { + fn drop(&mut self) { + let _ = self.close_fn.call0(&JsValue::NULL); + } +} + +fn generic(reason: String) -> v01::GenericError { + v01::GenericError::GenericError(v01::GenericErr { reason }) +} + +/// Await the JS callback's return value if it's a Promise; pass other +/// values through unchanged. Every host callback resolves through this so +/// the JS side is free to be sync or async. +async fn await_optional_promise(returned: JsValue) -> Result { + if returned.is_instance_of::() { + let promise = returned.unchecked_into::(); + wasm_bindgen_futures::JsFuture::from(promise) + .await + .map_err(js_to_string) + } else { + Ok(returned) + } +} + +fn invoke_navigate_to( + bridge: &JsBridge, + url: &str, +) -> impl std::future::Future> + Send { + let fn_ = bridge.navigate_to.clone(); + let url = url.to_string(); + SendWrapper::new(async move { + let arg = JsValue::from_str(&url); + let returned = fn_.call1(&JsValue::NULL, &arg).map_err(js_to_string)?; + await_optional_promise(returned).await.map(|_| ()) + }) +} + +fn invoke_unit( + fn_: &Function, + payload: Vec, +) -> impl std::future::Future> + Send { + let fn_ = fn_.clone(); + SendWrapper::new(async move { + let arg = Uint8Array::from(payload.as_slice()); + let returned = fn_.call1(&JsValue::NULL, &arg).map_err(js_to_string)?; + await_optional_promise(returned).await.map(|_| ()) + }) +} + +fn invoke_bool( + fn_: &Function, + payload: Vec, +) -> impl std::future::Future> + Send { + let fn_ = fn_.clone(); + SendWrapper::new(async move { + let arg = Uint8Array::from(payload.as_slice()); + let returned = fn_.call1(&JsValue::NULL, &arg).map_err(js_to_string)?; + let resolved = await_optional_promise(returned).await?; + Ok(resolved.as_bool().unwrap_or(false)) + }) +} + +fn invoke_local_storage_read( + bridge: &JsBridge, + key: &str, +) -> impl std::future::Future>, String>> + Send { + let fn_ = bridge.local_storage_read.clone(); + let key = key.to_string(); + SendWrapper::new(async move { + let arg = JsValue::from_str(&key); + let returned = fn_.call1(&JsValue::NULL, &arg).map_err(js_to_string)?; + let resolved = await_optional_promise(returned).await?; + if resolved.is_null() || resolved.is_undefined() { + return Ok(None); + } + let array = resolved.dyn_into::().map_err(|_| { + "localStorageRead must resolve to Uint8Array, null or undefined".to_string() + })?; + Ok(Some(array.to_vec())) + }) +} + +fn invoke_local_storage_write( + bridge: &JsBridge, + key: &str, + value: &[u8], +) -> impl std::future::Future> + Send { + let fn_ = bridge.local_storage_write.clone(); + let key = key.to_string(); + let value = value.to_vec(); + SendWrapper::new(async move { + let key_arg = JsValue::from_str(&key); + let value_arg = Uint8Array::from(value.as_slice()); + let returned = fn_ + .call2(&JsValue::NULL, &key_arg, &value_arg) + .map_err(js_to_string)?; + await_optional_promise(returned).await.map(|_| ()) + }) +} + +fn invoke_local_storage_clear( + bridge: &JsBridge, + key: &str, +) -> impl std::future::Future> + Send { + let fn_ = bridge.local_storage_clear.clone(); + let key = key.to_string(); + SendWrapper::new(async move { + let arg = JsValue::from_str(&key); + let returned = fn_.call1(&JsValue::NULL, &arg).map_err(js_to_string)?; + await_optional_promise(returned).await.map(|_| ()) + }) +} + +/// Invoke an async JS callback with a SCALE-encoded request. The callback +/// may return a `Uint8Array` of SCALE-encoded response bytes directly or +/// wrapped in a `Promise`. Any thrown error is converted to a string +/// reason for the corresponding domain error. +/// +/// `JsFuture` is `!Send`, so the body runs inside a `SendWrapper`. Safe +/// on wasm32 because the runtime is single-threaded. +fn invoke_request_response( + fn_: &Function, + request: Vec, +) -> impl std::future::Future> + Send +where + T: Decode, +{ + let fn_ = fn_.clone(); + SendWrapper::new(async move { + let arg = Uint8Array::from(request.as_slice()); + let returned = fn_.call1(&JsValue::NULL, &arg).map_err(js_to_string)?; + let resolved = await_optional_promise(returned).await?; + let bytes = resolved + .dyn_into::() + .map_err(|_| "callback must resolve to Uint8Array".to_string())? + .to_vec(); + T::decode(&mut &*bytes).map_err(|err| format!("failed to decode response: {err}")) + }) +} + +/// Invoke a JS subscription callback. The callback receives an optional +/// request payload followed by a `sendItem(bytes)` function used to push +/// SCALE-encoded items back to Rust, and returns an optional dispose +/// function. Dropping the stream disposes the subscription on the JS side. +fn invoke_subscription(fn_: &Function, request: Option>) -> BoxStream<'static, T> +where + T: Decode + Send + 'static, +{ + let (tx, rx) = mpsc::unbounded::>(); + let send_closure = Closure::wrap(Box::new(move |bytes: Uint8Array| { + let _ = tx.unbounded_send(bytes.to_vec()); + }) as Box); + + let call_result = match request { + Some(req) => { + let req_arg = Uint8Array::from(req.as_slice()); + fn_.call2( + &JsValue::NULL, + &req_arg, + send_closure.as_ref().unchecked_ref(), + ) + } + None => fn_.call1(&JsValue::NULL, send_closure.as_ref().unchecked_ref()), + }; + + let dispose = call_result.ok().and_then(|v| v.dyn_into::().ok()); + + let guard = SendWrapper::new(SubscriptionGuard { + _send: send_closure, + dispose, + }); + + futures::stream::unfold((rx, guard), |(mut rx, guard)| async move { + loop { + match rx.next().await { + Some(bytes) => match T::decode(&mut &*bytes) { + Ok(item) => return Some((item, (rx, guard))), + Err(err) => { + web_sys::console::error_1( + &format!("subscription item decode error: {err}").into(), + ); + } + }, + None => return None, + } + } + }) + .boxed() +} + +struct SubscriptionGuard { + _send: Closure, + dispose: Option, +} + +impl Drop for SubscriptionGuard { + fn drop(&mut self) { + if let Some(dispose) = self.dispose.take() { + let _ = dispose.call0(&JsValue::NULL); + } + } +} + +fn js_to_string(value: JsValue) -> String { + value + .as_string() + .or_else(|| { + value + .dyn_ref::() + .map(|err| err.message().into()) + }) + .unwrap_or_else(|| format!("{value:?}")) +} + +fn get_function(callbacks: &JsValue, name: &str) -> Result { + let value = Reflect::get(callbacks, &JsValue::from_str(name))?; + value + .dyn_into::() + .map_err(|_| JsValue::from_str(&format!("callbacks.{name} must be a function"))) +} + +fn get_optional_function(callbacks: &JsValue, name: &str) -> Result, JsValue> { + let value = Reflect::get(callbacks, &JsValue::from_str(name))?; + if value.is_null() || value.is_undefined() { + return Ok(None); + } + value + .dyn_into::() + .map(Some) + .map_err(|_| JsValue::from_str(&format!("callbacks.{name} must be a function"))) +} + +fn noop_function() -> Function { + Function::new_no_args("") +} + +struct WasmCoreInner { + core: TrUApiCore, + transport: Arc, + dispose_fn: SendWrapper, + disposed: Cell, + disposing: Cell, +} + +/// Toggle [`crate::debug_log`] output. Hosts read their `truapi:debug` +/// flag (web: localStorage) and call this once during boot. +#[wasm_bindgen(js_name = setDebugEnabled)] +pub fn set_debug_enabled(enabled: bool) { + crate::debug_log::set_enabled(enabled); +} + +/// JS-callable handle to the TrUAPI core. Constructed once per shell boot. +#[wasm_bindgen] +pub struct WasmTrUApiCore { + inner: Rc, +} + +#[wasm_bindgen] +impl WasmTrUApiCore { + /// Build the core from a JS callbacks object. The object must define + /// every host capability the [`truapi_platform::Platform`] trait set + /// requires (camelCase property names; see the source for the full + /// list). + #[wasm_bindgen(constructor)] + pub fn new(callbacks: JsValue) -> Result { + let bridge = Arc::new(JsBridge::from_js(&callbacks)?); + let disposed = Arc::new(AtomicBool::new(false)); + let transport = Arc::new(WasmCallbackTransport { + bridge: SendWrapper::new(bridge.clone()), + disposed: disposed.clone(), + }); + let dispose_fn = SendWrapper::new(bridge.dispose.clone()); + let platform = Arc::new(WasmPlatform::new(bridge)); + let core = TrUApiCore::from_platform(platform); + Ok(Self { + inner: Rc::new(WasmCoreInner { + core, + transport, + dispose_fn, + disposed: Cell::new(false), + disposing: Cell::new(false), + }), + }) + } + + /// Push a SCALE-encoded protocol frame into the dispatcher. Responses + /// (and subscription items) flow back through the `emitFrame` + /// callback. + #[wasm_bindgen(js_name = receiveFromProduct)] + pub async fn receive_from_product(&self, frame: Vec) -> Result<(), JsValue> { + if self.inner.disposed.get() { + return Ok(()); + } + + let message = ProtocolMessage::decode(&mut &*frame) + .map_err(|err| JsValue::from_str(&format!("invalid frame: {err}")))?; + + let transport: Arc = self.inner.transport.clone(); + self.inner.core.dispatch(message, transport).await; + Ok(()) + } + + /// Tear down the bridge. Invokes the JS-side `dispose` callback so the + /// host can drop its end of the wiring. + pub fn dispose(&self) -> Result<(), JsValue> { + if self.inner.disposing.replace(true) { + return Ok(()); + } + + self.inner.transport.disposed.store(true, Ordering::Relaxed); + + let result = self.inner.dispose_fn.call0(&JsValue::NULL).map(|_| ()); + + self.inner.disposed.set(true); + self.inner.disposing.set(false); + result + } + + /// Push the currently-paired session into the core. Called by the + /// host shell whenever the user pairs / unpairs. `pubkey` must be + /// exactly 32 bytes (sr25519 root public key); usernames may be + /// null / undefined when the identity record carries no value. + #[wasm_bindgen(js_name = setActiveSession)] + pub fn set_active_session( + &self, + pubkey: Vec, + lite_username: Option, + full_username: Option, + ) -> Result<(), JsValue> { + let public_key: [u8; 32] = pubkey.as_slice().try_into().map_err(|_| { + JsValue::from_str(&format!( + "setActiveSession: pubkey must be 32 bytes, got {}", + pubkey.len() + )) + })?; + self.inner + .core + .session_state() + .set_session(crate::host_logic::session::SessionInfo { + public_key, + lite_username, + full_username, + }); + Ok(()) + } + + /// Drop the currently-paired session. + #[wasm_bindgen(js_name = clearActiveSession)] + pub fn clear_active_session(&self) { + self.inner.core.session_state().clear_session(); + } +} diff --git a/rust/crates/truapi-server/src/ws_bridge.rs b/rust/crates/truapi-server/src/ws_bridge.rs new file mode 100644 index 00000000..4973d2c2 --- /dev/null +++ b/rust/crates/truapi-server/src/ws_bridge.rs @@ -0,0 +1,629 @@ +//! Localhost WebSocket bridge. Binds to `127.0.0.1:`, gates each +//! connection on a session token, and relays SCALE-encoded +//! [`ProtocolMessage`] frames into a [`TrUApiCore`]. +//! +//! Feature-gated (`ws-bridge`) so wasm32 and no-tokio build paths stay lean. +//! +//! The bridge owns a `tokio` runtime spawned at [`WsBridge::start`] time and +//! shuts down both the accept loop and the runtime when the handle is dropped +//! or [`WsBridge::stop`] is called. + +use std::io; +use std::net::SocketAddr; +use std::sync::{Arc, Mutex}; +use std::thread; + +use futures::{SinkExt, StreamExt}; +use parity_scale_codec::{Decode, Encode}; +use rand::RngCore; +use tokio::net::TcpListener; +use tokio::sync::{mpsc, oneshot}; +use tokio::task::LocalSet; +use tokio_tungstenite::tungstenite::Message as WsMessage; +use tokio_tungstenite::tungstenite::handshake::server::{ErrorResponse, Request, Response}; +use tokio_tungstenite::tungstenite::http::{Response as HttpResponse, StatusCode}; +use tokio_tungstenite::tungstenite::protocol::CloseFrame; +use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode; + +use crate::{ProtocolMessage, TrUApiCore, Transport}; + +/// Per-session descriptor returned to the host: product uses `port + token` +/// to build its WebSocket URL (e.g. `ws://127.0.0.1:/?t=`). +#[derive(Clone, Debug, uniffi::Record)] +pub struct WsBridgeEndpoint { + /// Localhost port the bridge is listening on. + pub port: u16, + /// Session token; the connecting client must supply this as the + /// `?t=` query parameter to be accepted. + pub token: String, +} + +/// Failure modes returned from host-facing `start_ws_bridge` wrappers. +#[derive(Debug, thiserror::Error, uniffi::Error)] +#[uniffi(flat_error)] +pub enum WsBridgeStartError { + /// A bridge is already running for this host. + #[error("ws bridge already running")] + AlreadyRunning, + /// Anything else (bind failure, runtime spin-up failure, ...). + #[error("ws bridge start failed: {0}")] + Io(String), +} + +impl From for WsBridgeStartError { + fn from(err: io::Error) -> Self { + if err.kind() == io::ErrorKind::AlreadyExists { + WsBridgeStartError::AlreadyRunning + } else { + WsBridgeStartError::Io(err.to_string()) + } + } +} + +/// Logger callback shape used by the bridge for lifecycle events. The +/// Android and iOS wrappers adapt their per-platform callback interfaces to +/// this platform-neutral shape. +pub type BridgeLogger = Arc; + +/// Running bridge handle. Drop or call [`WsBridge::stop`] to shut down. +/// +/// The bridge owns a dedicated OS thread that runs a `tokio` current-thread +/// runtime + `LocalSet`. Using `spawn_local` is required because the +/// dispatcher's per-method futures are `LocalBoxFuture` (the truapi trait +/// uses `async fn`, whose auto-generated futures are not `Send`). +pub struct WsBridge { + shutdown: Option>, + thread: Option>, +} + +impl WsBridge { + /// Bind a localhost listener and start the accept loop on a dedicated + /// OS thread. Returns the [`WsBridgeEndpoint`] descriptor the host + /// hands to the product alongside the bridge handle. + pub fn start( + bind_port: u16, + core: Arc, + logger: BridgeLogger, + ) -> io::Result<(Self, WsBridgeEndpoint)> { + let mut token_bytes = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut token_bytes); + let token = hex::encode(token_bytes); + + // Bind synchronously so we can surface bind errors back to the + // caller and discover the actual port the OS handed back. The + // listener is registered with tokio inside the worker thread + // because a `tokio::net::TcpListener` is bound to the runtime that + // created it. + let std_listener = + std::net::TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], bind_port)))?; + std_listener.set_nonblocking(true)?; + let port = std_listener.local_addr()?.port(); + + let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); + let (ready_tx, ready_rx) = std::sync::mpsc::channel::>(); + let accept_token = token.clone(); + let accept_logger = logger.clone(); + let worker_logger = logger.clone(); + let thread = thread::Builder::new() + .name("truapi-ws-bridge".to_string()) + .spawn(move || { + let rt = match tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + { + Ok(rt) => rt, + Err(err) => { + let _ = ready_tx.send(Err(io::Error::other(err.to_string()))); + return; + } + }; + let local = LocalSet::new(); + let listener_setup = rt.block_on(async { TcpListener::from_std(std_listener) }); + let listener = match listener_setup { + Ok(listener) => { + let _ = ready_tx.send(Ok(())); + listener + } + Err(err) => { + let _ = ready_tx.send(Err(err)); + return; + } + }; + local.block_on( + &rt, + accept_loop(listener, core, accept_token, accept_logger, shutdown_rx), + ); + worker_logger("truapi.ws_bridge.worker_exit", "worker thread exiting"); + })?; + + // Block until the worker thread reports the listener is registered + // with its runtime, so the caller knows the bridge is ready to + // accept connections by the time `start` returns. + match ready_rx.recv() { + Ok(Ok(())) => {} + Ok(Err(err)) => return Err(err), + Err(err) => return Err(io::Error::other(err.to_string())), + } + + logger( + "truapi.ws_bridge.started", + &format!("port={port} token_len={}", token.len()), + ); + + Ok(( + Self { + shutdown: Some(shutdown_tx), + thread: Some(thread), + }, + WsBridgeEndpoint { port, token }, + )) + } + + /// Signal the accept loop to exit and join the worker thread. + pub fn stop(&mut self) { + if let Some(tx) = self.shutdown.take() { + let _ = tx.send(()); + } + if let Some(handle) = self.thread.take() { + let _ = handle.join(); + } + } +} + +impl Drop for WsBridge { + fn drop(&mut self) { + self.stop(); + } +} + +async fn accept_loop( + listener: TcpListener, + core: Arc, + expected_token: String, + logger: BridgeLogger, + mut shutdown: oneshot::Receiver<()>, +) { + let mut handles: Vec> = Vec::new(); + loop { + tokio::select! { + _ = &mut shutdown => { + logger("truapi.ws_bridge.shutdown", "accept loop exiting"); + for h in &handles { + h.abort(); + } + break; + } + accepted = listener.accept() => { + let (stream, peer) = match accepted { + Ok(pair) => pair, + Err(err) => { + logger("truapi.ws_bridge.accept_error", &err.to_string()); + continue; + } + }; + let core = core.clone(); + let logger = logger.clone(); + let expected = expected_token.clone(); + handles.push(tokio::task::spawn_local(async move { + handle_connection(stream, peer, core, expected, logger).await; + })); + handles.retain(|h| !h.is_finished()); + } + } + } +} + +async fn handle_connection( + stream: tokio::net::TcpStream, + peer: SocketAddr, + core: Arc, + expected_token: String, + logger: BridgeLogger, +) { + let auth_logger = logger.clone(); + let callback = |req: &Request, resp: Response| -> Result { + if path_token_matches( + req.uri().path_and_query().map(|p| p.as_str()), + &expected_token, + ) { + Ok(resp) + } else { + auth_logger("truapi.ws_bridge.reject_unauthorized", &peer.to_string()); + let mut err: ErrorResponse = HttpResponse::new(Some("invalid token".to_string())); + *err.status_mut() = StatusCode::UNAUTHORIZED; + Err(err) + } + }; + + let ws = match tokio_tungstenite::accept_hdr_async(stream, callback).await { + Ok(ws) => ws, + Err(err) => { + logger("truapi.ws_bridge.handshake_error", &err.to_string()); + return; + } + }; + + logger("truapi.ws_bridge.connection_open", &peer.to_string()); + let (mut sink, mut source) = ws.split(); + let (out_tx, mut out_rx) = mpsc::unbounded_channel::>(); + let transport: Arc = Arc::new(WsTransport::new(out_tx)); + + let pump_logger = logger.clone(); + let pump = tokio::task::spawn_local(async move { + while let Some(bytes) = out_rx.recv().await { + if let Err(err) = sink.send(WsMessage::Binary(bytes)).await { + pump_logger("truapi.ws_bridge.send_error", &err.to_string()); + break; + } + } + let _ = sink + .send(WsMessage::Close(Some(CloseFrame { + code: CloseCode::Normal, + reason: "bridge closing".into(), + }))) + .await; + let _ = sink.close().await; + }); + + while let Some(frame) = source.next().await { + match frame { + Ok(WsMessage::Binary(bytes)) => { + let message = match ProtocolMessage::decode(&mut &*bytes) { + Ok(m) => m, + Err(err) => { + logger("truapi.ws_bridge.decode_error", &err.to_string()); + continue; + } + }; + core.dispatch(message, transport.clone()).await; + } + Ok(WsMessage::Text(_)) => { + logger("truapi.ws_bridge.text_frame_ignored", ""); + } + Ok(WsMessage::Close(_)) => break, + Ok(_) => {} + Err(err) => { + logger("truapi.ws_bridge.read_error", &err.to_string()); + break; + } + } + } + + drop(transport); + let _ = pump.await; + logger("truapi.ws_bridge.connection_closed", &peer.to_string()); +} + +fn path_token_matches(path_and_query: Option<&str>, expected: &str) -> bool { + let Some(raw) = path_and_query else { + return false; + }; + let query = match raw.find('?') { + Some(idx) => &raw[idx + 1..], + None => return false, + }; + for pair in query.split('&') { + let (key, value) = match pair.split_once('=') { + Some(kv) => kv, + None => continue, + }; + if key == "t" && value == expected { + return true; + } + } + false +} + +struct WsTransport { + outbound: mpsc::UnboundedSender>, + closed: Mutex, +} + +impl WsTransport { + fn new(outbound: mpsc::UnboundedSender>) -> Self { + Self { + outbound, + closed: Mutex::new(false), + } + } +} + +impl Transport for WsTransport { + fn send(&self, message: ProtocolMessage) { + if *self.closed.lock().unwrap() { + return; + } + if self.outbound.send(message.encode()).is_err() { + *self.closed.lock().unwrap() = true; + } + } + + fn on_message( + &self, + _handler: Box, + ) -> Box { + Box::new(|| {}) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use async_trait::async_trait; + use futures::stream::{self, BoxStream}; + use parity_scale_codec::Encode; + use truapi::v01; + use truapi::versioned::account::{ + HostAccountConnectionStatusSubscribeItem, HostAccountCreateProofRequest, + HostAccountCreateProofResponse, HostAccountGetAliasRequest, HostAccountGetAliasResponse, + HostAccountGetRequest, HostAccountGetResponse, HostGetLegacyAccountsRequest, + HostGetLegacyAccountsResponse, HostGetUserIdRequest, HostGetUserIdResponse, + }; + use truapi::versioned::preimage::{ + RemotePreimageLookupSubscribeItem, RemotePreimageLookupSubscribeRequest, + }; + use truapi::versioned::signing::{ + HostSignPayloadRequest, HostSignPayloadResponse, HostSignRawRequest, HostSignRawResponse, + }; + use truapi::versioned::statement_store::{ + RemoteStatementStoreCreateProofRequest, RemoteStatementStoreCreateProofResponse, + RemoteStatementStoreSubmitRequest, RemoteStatementStoreSubscribeItem, + RemoteStatementStoreSubscribeRequest, + }; + use truapi::versioned::system::{HostFeatureSupportedRequest, HostFeatureSupportedResponse}; + use truapi_platform::{ + Accounts as PlatformAccounts, ChainProvider, Features, GenesisHash, JsonRpcConnection, + Navigation, Notifications, Permissions, Preimage as PlatformPreimage, + Signing as PlatformSigning, StatementStore as PlatformStatementStore, Storage, + }; + + use crate::frame::{FrameKind, Payload, compose_action}; + + struct StubPlatform; + + #[async_trait] + impl Storage for StubPlatform { + async fn read( + &self, + _key: String, + ) -> Result>, v01::HostLocalStorageReadError> { + Ok(None) + } + async fn write( + &self, + _key: String, + _value: Vec, + ) -> Result<(), v01::HostLocalStorageReadError> { + Ok(()) + } + async fn clear(&self, _key: String) -> Result<(), v01::HostLocalStorageReadError> { + Ok(()) + } + } + + #[async_trait] + impl Navigation for StubPlatform { + async fn navigate_to(&self, _url: String) -> Result<(), v01::HostNavigateToError> { + Ok(()) + } + } + + #[async_trait] + impl Notifications for StubPlatform { + async fn push_notification( + &self, + _notification: v01::HostPushNotificationRequest, + ) -> Result<(), v01::GenericError> { + Ok(()) + } + } + + #[async_trait] + impl Permissions for StubPlatform { + async fn device_permission( + &self, + _request: v01::HostDevicePermissionRequest, + ) -> Result { + Ok(v01::HostDevicePermissionResponse { granted: true }) + } + async fn remote_permission( + &self, + _request: v01::RemotePermissionRequest, + ) -> Result { + Ok(v01::RemotePermissionResponse { granted: true }) + } + } + + #[async_trait] + impl Features for StubPlatform { + async fn feature_supported( + &self, + request: HostFeatureSupportedRequest, + ) -> Result { + let HostFeatureSupportedRequest::V1(_) = request; + Ok(HostFeatureSupportedResponse::V1( + v01::HostFeatureSupportedResponse { supported: true }, + )) + } + } + + struct DeadConnection; + impl JsonRpcConnection for DeadConnection { + fn send(&self, _request: String) {} + fn responses(&self) -> BoxStream<'static, String> { + Box::pin(stream::empty()) + } + } + + #[async_trait] + impl ChainProvider for StubPlatform { + async fn connect( + &self, + _genesis_hash: GenesisHash, + ) -> Result, v01::GenericError> { + Ok(Box::new(DeadConnection)) + } + } + + #[async_trait] + impl PlatformAccounts for StubPlatform { + async fn host_account_get( + &self, + _request: HostAccountGetRequest, + ) -> Result { + Err(v01::HostAccountGetError::NotConnected) + } + async fn host_account_get_alias( + &self, + _request: HostAccountGetAliasRequest, + ) -> Result { + Err(v01::HostAccountGetError::NotConnected) + } + async fn host_account_create_proof( + &self, + _request: HostAccountCreateProofRequest, + ) -> Result { + Err(v01::HostAccountCreateProofError::RingNotFound) + } + async fn host_get_legacy_accounts( + &self, + _request: HostGetLegacyAccountsRequest, + ) -> Result { + Ok(HostGetLegacyAccountsResponse::V1( + v01::HostGetLegacyAccountsResponse { accounts: vec![] }, + )) + } + async fn host_account_connection_status_subscribe( + &self, + ) -> BoxStream<'static, HostAccountConnectionStatusSubscribeItem> { + Box::pin(stream::empty()) + } + async fn host_get_user_id( + &self, + _request: HostGetUserIdRequest, + ) -> Result { + Err(v01::HostGetUserIdError::NotConnected) + } + } + + #[async_trait] + impl PlatformSigning for StubPlatform { + async fn host_sign_payload( + &self, + _request: HostSignPayloadRequest, + ) -> Result { + Err(v01::HostSignPayloadError::Rejected) + } + async fn host_sign_raw( + &self, + _request: HostSignRawRequest, + ) -> Result { + Err(v01::HostSignPayloadError::Rejected) + } + } + + #[async_trait] + impl PlatformStatementStore for StubPlatform { + async fn remote_statement_store_subscribe( + &self, + _request: RemoteStatementStoreSubscribeRequest, + ) -> BoxStream<'static, RemoteStatementStoreSubscribeItem> { + Box::pin(stream::empty()) + } + async fn remote_statement_store_submit( + &self, + _request: RemoteStatementStoreSubmitRequest, + ) -> Result<(), v01::GenericError> { + Ok(()) + } + async fn remote_statement_store_create_proof( + &self, + _request: RemoteStatementStoreCreateProofRequest, + ) -> Result< + RemoteStatementStoreCreateProofResponse, + v01::RemoteStatementStoreCreateProofError, + > { + Err(v01::RemoteStatementStoreCreateProofError::UnableToSign) + } + } + + #[async_trait] + impl PlatformPreimage for StubPlatform { + async fn remote_preimage_lookup_subscribe( + &self, + _request: RemotePreimageLookupSubscribeRequest, + ) -> BoxStream<'static, RemotePreimageLookupSubscribeItem> { + Box::pin(stream::empty()) + } + } + + #[test] + fn path_token_matches_exact() { + assert!(path_token_matches(Some("/?t=abc"), "abc")); + assert!(path_token_matches(Some("/?foo=1&t=abc"), "abc")); + assert!(!path_token_matches(Some("/?t=other"), "abc")); + assert!(!path_token_matches(Some("/?token=abc"), "abc")); + assert!(!path_token_matches(Some("/"), "abc")); + assert!(!path_token_matches(None, "abc")); + } + + /// Spin the bridge up on `127.0.0.1:0`, dial it with a real + /// `tokio-tungstenite` client, send a known SCALE frame, and verify + /// the bridge echoes the SCALE-encoded `feature_supported` response. + #[test] + fn round_trip_feature_supported_through_bridge() { + let core = Arc::new(TrUApiCore::from_platform(Arc::new(StubPlatform))); + let logger: BridgeLogger = Arc::new(|_, _| {}); + let (mut bridge, endpoint) = WsBridge::start(0, core, logger).expect("start bridge"); + let url = format!("ws://127.0.0.1:{}/?t={}", endpoint.port, endpoint.token); + + // Use a fresh `tokio` runtime on the test thread so we don't fight + // the bridge's runtime, which lives on a different worker thread. + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("test runtime"); + + let response_bytes = rt.block_on(async { + let (mut ws, _) = tokio_tungstenite::connect_async(&url).await.expect("dial"); + + let request_frame = ProtocolMessage { + request_id: "p:1".into(), + payload: Payload { + tag: compose_action("system_feature_supported", FrameKind::Request), + value: HostFeatureSupportedRequest::V1( + v01::HostFeatureSupportedRequest::Chain { + genesis_hash: vec![0u8; 32], + }, + ) + .encode(), + }, + }; + ws.send(WsMessage::Binary(request_frame.encode())) + .await + .expect("send"); + + // Block until the bridge replies with the response frame. + loop { + match ws.next().await { + Some(Ok(WsMessage::Binary(bytes))) => break bytes, + Some(Ok(_)) => continue, + Some(Err(err)) => panic!("ws error: {err}"), + None => panic!("connection closed before response"), + } + } + }); + + let response = ProtocolMessage::decode(&mut &response_bytes[..]).expect("decode response"); + assert_eq!(response.request_id, "p:1"); + assert_eq!( + response.payload.tag, + compose_action("system_feature_supported", FrameKind::Response), + ); + // Wire payload is `Result`-shaped: + // [Ok disc=0x00][V1 variant 0x00][supported=1] + assert_eq!(response.payload.value, vec![0x00, 0x00, 0x01]); + + bridge.stop(); + } +} diff --git a/rust/crates/truapi-server/tests/golden_frame.rs b/rust/crates/truapi-server/tests/golden_frame.rs new file mode 100644 index 00000000..4c4588ea --- /dev/null +++ b/rust/crates/truapi-server/tests/golden_frame.rs @@ -0,0 +1,52 @@ +//! Binary golden-frame regression test. +//! +//! Loads `tests/snapshots/golden-account-get.bin` (the captured raw bytes +//! of an `account_get_account_request` frame) and asserts that +//! `ProtocolMessage::decode` produces the expected in-memory shape. +//! +//! The frame encodes: +//! requestId = "p:1" +//! payload = account_get_account_request, +//! inner = HostAccountGetRequest::V1(("foo", 0u32)) +//! +//! On the wire (14 bytes): +//! [0c 70 3a 31] requestId = compact-len(3) + "p:1" +//! [16] discriminant 22 = account_get_account_request +//! [00] versioned wrapper variant V1 +//! [0c 66 6f 6f] "foo" +//! [00 00 00 00] u32 = 0 +//! +//! If this test fails after a wire-protocol change, regenerate the file +//! deliberately and re-check the change against the wire table. + +use parity_scale_codec::{Decode, Encode}; +use truapi_server::{Payload, ProtocolMessage}; + +const GOLDEN: &[u8] = include_bytes!("snapshots/golden-account-get.bin"); + +#[test] +fn golden_account_get_frame_decodes_to_expected_message() { + let decoded = ProtocolMessage::decode(&mut &GOLDEN[..]) + .expect("golden frame must decode with the current wire codec"); + + let mut expected_inner = Vec::new(); + expected_inner.push(0x00u8); // V1 variant + "foo".to_string().encode_to(&mut expected_inner); + 0u32.encode_to(&mut expected_inner); + + let expected = ProtocolMessage { + request_id: "p:1".to_string(), + payload: Payload { + tag: "account_get_account_request".to_string(), + value: expected_inner, + }, + }; + assert_eq!(decoded, expected); +} + +#[test] +fn golden_account_get_frame_round_trips() { + // Encoding the in-memory shape must reproduce the on-disk bytes exactly. + let decoded = ProtocolMessage::decode(&mut &GOLDEN[..]).expect("decode"); + assert_eq!(decoded.encode(), GOLDEN); +} diff --git a/rust/crates/truapi-server/tests/snapshots/golden-account-get.bin b/rust/crates/truapi-server/tests/snapshots/golden-account-get.bin new file mode 100644 index 0000000000000000000000000000000000000000..c66be11b9bf19e8c751b7faa4996bf36cd7e90b4 GIT binary patch literal 14 Tcmd-nurd^5;7QBRX8-~K6a)fJ literal 0 HcmV?d00001 diff --git a/rust/crates/truapi-server/tests/wire_result_shape.rs b/rust/crates/truapi-server/tests/wire_result_shape.rs new file mode 100644 index 00000000..f7acf693 --- /dev/null +++ b/rust/crates/truapi-server/tests/wire_result_shape.rs @@ -0,0 +1,295 @@ +//! Result-wire-shape regression test. +//! +//! The TS host/client codec expects every request response to be +//! `Result`-shaped on the wire (one leading discriminant byte +//! followed by the SCALE-encoded value). This test stands up a +//! `TrUApiCore::from_platform` with a `StubPlatform` whose `Features` +//! impl returns `Ok(supported = true)` and asserts: +//! +//! - A `system_feature_supported_request` produces a response whose +//! payload begins with `0x00` (Ok), followed by the encoded +//! `HostFeatureSupportedResponse::V1(true)`. +//! - A `local_storage_read_request` whose stub returns +//! `Err(HostLocalStorageReadError::Full)` produces a response whose +//! payload begins with `0x01` (Err), followed by the encoded +//! `CallError::Domain(Full)`. +//! +//! Both halves prove the wire layout stays in lockstep with the TS +//! `S.Result(ok, err)` codec. + +use std::sync::Arc; + +use async_trait::async_trait; +use futures::stream::{self, BoxStream}; +use parity_scale_codec::{Decode, Encode}; + +use truapi::v01; +use truapi::versioned::account::{ + HostAccountConnectionStatusSubscribeItem, HostAccountCreateProofRequest, + HostAccountCreateProofResponse, HostAccountGetAliasRequest, HostAccountGetAliasResponse, + HostAccountGetRequest, HostAccountGetResponse, HostGetLegacyAccountsRequest, + HostGetLegacyAccountsResponse, HostGetUserIdRequest, HostGetUserIdResponse, +}; +use truapi::versioned::preimage::{ + RemotePreimageLookupSubscribeItem, RemotePreimageLookupSubscribeRequest, +}; +use truapi::versioned::signing::{ + HostSignPayloadRequest, HostSignPayloadResponse, HostSignRawRequest, HostSignRawResponse, +}; +use truapi::versioned::statement_store::{ + RemoteStatementStoreCreateProofRequest, RemoteStatementStoreCreateProofResponse, + RemoteStatementStoreSubmitRequest, RemoteStatementStoreSubscribeItem, + RemoteStatementStoreSubscribeRequest, +}; +use truapi::versioned::system::{HostFeatureSupportedRequest, HostFeatureSupportedResponse}; + +use truapi_platform::{ + Accounts, ChainProvider, Features, GenesisHash, JsonRpcConnection, Navigation, Notifications, + Permissions, Preimage, Signing, StatementStore, Storage, +}; + +use truapi_server::{FrameKind, Payload, ProtocolMessage, TrUApiCore, compose_action}; + +struct StubPlatform; + +#[async_trait] +impl Storage for StubPlatform { + async fn read(&self, _key: String) -> Result>, v01::HostLocalStorageReadError> { + // Drive the error-path test: return `Full` so we can assert the + // wire-Err discriminant precedes the SCALE-encoded `CallError::Domain(Full)`. + Err(v01::HostLocalStorageReadError::Full) + } + async fn write( + &self, + _key: String, + _value: Vec, + ) -> Result<(), v01::HostLocalStorageReadError> { + Ok(()) + } + async fn clear(&self, _key: String) -> Result<(), v01::HostLocalStorageReadError> { + Ok(()) + } +} + +#[async_trait] +impl Navigation for StubPlatform { + async fn navigate_to(&self, _url: String) -> Result<(), v01::HostNavigateToError> { + Ok(()) + } +} + +#[async_trait] +impl Notifications for StubPlatform { + async fn push_notification( + &self, + _notification: v01::HostPushNotificationRequest, + ) -> Result<(), v01::GenericError> { + Ok(()) + } +} + +#[async_trait] +impl Permissions for StubPlatform { + async fn device_permission( + &self, + _request: v01::HostDevicePermissionRequest, + ) -> Result { + Ok(v01::HostDevicePermissionResponse { granted: true }) + } + async fn remote_permission( + &self, + _request: v01::RemotePermissionRequest, + ) -> Result { + Ok(v01::RemotePermissionResponse { granted: true }) + } +} + +#[async_trait] +impl Features for StubPlatform { + async fn feature_supported( + &self, + _request: HostFeatureSupportedRequest, + ) -> Result { + Ok(HostFeatureSupportedResponse::V1( + v01::HostFeatureSupportedResponse { supported: true }, + )) + } +} + +struct DeadConnection; +impl JsonRpcConnection for DeadConnection { + fn send(&self, _request: String) {} + fn responses(&self) -> BoxStream<'static, String> { + Box::pin(stream::empty()) + } +} + +#[async_trait] +impl ChainProvider for StubPlatform { + async fn connect( + &self, + _genesis_hash: GenesisHash, + ) -> Result, v01::GenericError> { + Ok(Box::new(DeadConnection)) + } +} + +#[async_trait] +impl Accounts for StubPlatform { + async fn host_account_get( + &self, + _request: HostAccountGetRequest, + ) -> Result { + Err(v01::HostAccountGetError::NotConnected) + } + async fn host_account_get_alias( + &self, + _request: HostAccountGetAliasRequest, + ) -> Result { + Err(v01::HostAccountGetError::NotConnected) + } + async fn host_account_create_proof( + &self, + _request: HostAccountCreateProofRequest, + ) -> Result { + Err(v01::HostAccountCreateProofError::RingNotFound) + } + async fn host_get_legacy_accounts( + &self, + _request: HostGetLegacyAccountsRequest, + ) -> Result { + Ok(HostGetLegacyAccountsResponse::V1( + v01::HostGetLegacyAccountsResponse { accounts: vec![] }, + )) + } + async fn host_account_connection_status_subscribe( + &self, + ) -> BoxStream<'static, HostAccountConnectionStatusSubscribeItem> { + Box::pin(stream::empty()) + } + async fn host_get_user_id( + &self, + _request: HostGetUserIdRequest, + ) -> Result { + Err(v01::HostGetUserIdError::NotConnected) + } +} + +#[async_trait] +impl Signing for StubPlatform { + async fn host_sign_payload( + &self, + _request: HostSignPayloadRequest, + ) -> Result { + Err(v01::HostSignPayloadError::Rejected) + } + async fn host_sign_raw( + &self, + _request: HostSignRawRequest, + ) -> Result { + Err(v01::HostSignPayloadError::Rejected) + } +} + +#[async_trait] +impl StatementStore for StubPlatform { + async fn remote_statement_store_subscribe( + &self, + _request: RemoteStatementStoreSubscribeRequest, + ) -> BoxStream<'static, RemoteStatementStoreSubscribeItem> { + Box::pin(stream::empty()) + } + async fn remote_statement_store_submit( + &self, + _request: RemoteStatementStoreSubmitRequest, + ) -> Result<(), v01::GenericError> { + Ok(()) + } + async fn remote_statement_store_create_proof( + &self, + _request: RemoteStatementStoreCreateProofRequest, + ) -> Result + { + Err(v01::RemoteStatementStoreCreateProofError::UnableToSign) + } +} + +#[async_trait] +impl Preimage for StubPlatform { + async fn remote_preimage_lookup_subscribe( + &self, + _request: RemotePreimageLookupSubscribeRequest, + ) -> BoxStream<'static, RemotePreimageLookupSubscribeItem> { + Box::pin(stream::empty()) + } +} + +fn dispatch(core: &TrUApiCore, frame: ProtocolMessage) -> ProtocolMessage { + let encoded = frame.encode(); + let response_bytes = core + .receive_from_product(&encoded) + .expect("dispatcher emitted a response frame"); + ProtocolMessage::decode(&mut &response_bytes[..]).expect("decode response") +} + +#[test] +fn feature_supported_ok_response_uses_ok_discriminant() { + let core = TrUApiCore::from_platform(Arc::new(StubPlatform)); + let request = HostFeatureSupportedRequest::V1(v01::HostFeatureSupportedRequest::Chain { + genesis_hash: vec![0u8; 32], + }); + let frame = ProtocolMessage { + request_id: "p:1".into(), + payload: Payload { + tag: compose_action("system_feature_supported", FrameKind::Request), + value: request.encode(), + }, + }; + let response = dispatch(&core, frame); + assert_eq!(response.request_id, "p:1"); + assert_eq!( + response.payload.tag, + compose_action("system_feature_supported", FrameKind::Response), + ); + + // Wire payload: [Ok disc=0x00][encoded versioned response] + let mut expected = vec![0x00u8]; + HostFeatureSupportedResponse::V1(v01::HostFeatureSupportedResponse { supported: true }) + .encode_to(&mut expected); + assert_eq!(response.payload.value, expected); + // The Result-disc byte is unambiguously 0x00 for Ok. + assert_eq!(response.payload.value.first(), Some(&0x00)); +} + +#[test] +fn local_storage_read_err_response_uses_err_discriminant() { + let core = TrUApiCore::from_platform(Arc::new(StubPlatform)); + let request = truapi::versioned::local_storage::HostLocalStorageReadRequest::V1( + v01::HostLocalStorageReadRequest { + key: "missing".to_string(), + }, + ); + let frame = ProtocolMessage { + request_id: "p:2".into(), + payload: Payload { + tag: compose_action("local_storage_read", FrameKind::Request), + value: request.encode(), + }, + }; + let response = dispatch(&core, frame); + assert_eq!(response.request_id, "p:2"); + assert_eq!( + response.payload.tag, + compose_action("local_storage_read", FrameKind::Response), + ); + + // Wire payload: `[Err disc=0x01][SCALE-encoded CallError]`. The stub + // returns `HostLocalStorageReadError::Full`; the runtime wraps it in + // `CallError::Domain(HostLocalStorageReadError::V1(Full))`, encoded as: + // [0x01 Err disc] + // [0x00 CallError::Domain variant] + // [0x00 HostLocalStorageReadError::V1 variant] + // [0x00 v01::HostLocalStorageReadError::Full variant] + assert_eq!(response.payload.value, vec![0x01, 0x00, 0x00, 0x00]); + assert_eq!(response.payload.value.first(), Some(&0x01)); +} diff --git a/rust/crates/truapi-server/tests/wire_table_ts_parity.rs b/rust/crates/truapi-server/tests/wire_table_ts_parity.rs new file mode 100644 index 00000000..1134feab --- /dev/null +++ b/rust/crates/truapi-server/tests/wire_table_ts_parity.rs @@ -0,0 +1,175 @@ +//! Cross-language parity check: the Rust `WIRE_TABLE` and the TS +//! `wire-table.ts` must list the exact same `(method, request_id, response_id)` +//! tuples in the same order. A drift here means a product built against one +//! side will fail to decode frames produced by the other. +//! +//! Both files are auto-generated text artifacts of `truapi-codegen`; the +//! parser is a small line scanner so the test runs as part of `cargo test` +//! without any node/bun dependency. +//! +//! The TS file lives under `js/packages/truapi/src/generated/wire-table.ts` +//! and is `.gitignore`d (regenerated by `scripts/codegen.sh`). When the +//! generated file is absent, the test logs a skip notice and passes; once +//! the TS codegen lands and the file is regenerated, the parity check +//! becomes load-bearing automatically. + +use std::path::PathBuf; + +const RUST_TABLE: &str = include_str!("../src/generated/wire_table.rs"); + +#[derive(Debug, PartialEq, Eq)] +struct Row { + method: String, + request_or_start: u8, + response_or_receive: u8, + is_subscription: bool, +} + +fn parse_rust(src: &str) -> Vec { + // Walks the `pub const WIRE_TABLE: &[WireEntry] = &[ ... ];` body and + // pulls out each `WireEntry { method: "x", kind: ... }`. + let mut out = Vec::new(); + let mut iter = src.lines().peekable(); + while let Some(line) = iter.next() { + let trimmed = line.trim(); + let Some(rest) = trimmed.strip_prefix("method: \"") else { + continue; + }; + let Some(end) = rest.find('"') else { continue }; + let method = rest[..end].to_string(); + let mut request_or_start = None; + let mut response_or_receive = None; + let mut is_subscription = false; + for inner in iter.by_ref() { + let t = inner.trim(); + if t.starts_with("WireKind::Subscription") { + is_subscription = true; + } + if let Some(rest) = t + .strip_prefix("request_id: ") + .or_else(|| t.strip_prefix("start_id: ")) + { + let n: u8 = rest.trim_end_matches(',').parse().unwrap_or(0); + request_or_start = Some(n); + } + if let Some(rest) = t + .strip_prefix("response_id: ") + .or_else(|| t.strip_prefix("receive_id: ")) + { + let n: u8 = rest.trim_end_matches(',').parse().unwrap_or(0); + response_or_receive = Some(n); + } + if t == "}," + && let (Some(rs), Some(rr)) = (request_or_start, response_or_receive) + { + out.push(Row { + method, + request_or_start: rs, + response_or_receive: rr, + is_subscription, + }); + break; + } + } + } + out +} + +fn parse_ts(src: &str) -> Vec { + // Recognises the TS shape emitted by truapi-codegen, which (per the + // current codegen.rs) writes one object per method with literal + // numeric ids. The exact key names may evolve; this scanner accepts + // both the request/response and start/stop/interrupt/receive shapes. + let mut out = Vec::new(); + let mut iter = src.lines().peekable(); + while let Some(line) = iter.next() { + let trimmed = line.trim(); + let Some(rest) = trimmed + .strip_prefix("method: '") + .or_else(|| trimmed.strip_prefix("method: \"")) + else { + continue; + }; + let Some(end) = rest.find(['\'', '"']) else { + continue; + }; + let method = rest[..end].to_string(); + let mut request_or_start = None; + let mut response_or_receive = None; + let mut is_subscription = false; + for inner in iter.by_ref() { + let t = inner.trim(); + if t.contains("subscription") || t.contains("start:") { + is_subscription = true; + } + if let Some(rest) = t + .strip_prefix("request: ") + .or_else(|| t.strip_prefix("start: ")) + { + let n: u8 = rest.trim_end_matches(',').parse().unwrap_or(0); + request_or_start = Some(n); + } + if let Some(rest) = t + .strip_prefix("response: ") + .or_else(|| t.strip_prefix("receive: ")) + { + let n: u8 = rest.trim_end_matches(',').parse().unwrap_or(0); + response_or_receive = Some(n); + } + if t == "}," || t == "}" { + if let (Some(rs), Some(rr)) = (request_or_start, response_or_receive) { + out.push(Row { + method, + request_or_start: rs, + response_or_receive: rr, + is_subscription, + }); + } + break; + } + } + } + out +} + +#[test] +fn rust_and_ts_wire_tables_agree() { + let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let ts_path = manifest + .join("../../../js/packages/truapi/src/generated/wire-table.ts") + .canonicalize(); + + let ts_path = match ts_path { + Ok(p) => p, + Err(_) => { + eprintln!( + "skipping wire-table parity check: TS wire-table.ts is not present \ + (run scripts/codegen.sh once the TS codegen is wired up)" + ); + return; + } + }; + + let ts_src = match std::fs::read_to_string(&ts_path) { + Ok(s) => s, + Err(_) => { + eprintln!( + "skipping wire-table parity check: could not read {}", + ts_path.display() + ); + return; + } + }; + + let rust_rows = parse_rust(RUST_TABLE); + let ts_rows = parse_ts(&ts_src); + assert!( + !rust_rows.is_empty(), + "rust parser produced no entries; wire_table.rs format may have changed" + ); + assert_eq!( + rust_rows, ts_rows, + "Rust WIRE_TABLE and TS wire-table.ts diverged. Regenerate both via \ + `scripts/codegen.sh` so the codegen pipeline produces them in lockstep.", + ); +} diff --git a/rust/crates/truapi/src/v01/permissions.rs b/rust/crates/truapi/src/v01/permissions.rs index ebaf5c88..ba8ea2d7 100644 --- a/rust/crates/truapi/src/v01/permissions.rs +++ b/rust/crates/truapi/src/v01/permissions.rs @@ -1,3 +1,4 @@ +use core::fmt::{self, Display, Formatter}; use parity_scale_codec::{Decode, Encode}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] @@ -14,6 +15,23 @@ pub enum HostDevicePermissionRequest { Biometrics, } +impl Display for HostDevicePermissionRequest { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let label = match self { + Self::Notifications => "notifications", + Self::Camera => "camera", + Self::Microphone => "microphone", + Self::Bluetooth => "bluetooth", + Self::NFC => "NFC", + Self::Location => "location", + Self::Clipboard => "clipboard", + Self::OpenUrl => "open URL", + Self::Biometrics => "biometrics", + }; + f.write_str(label) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub enum RemotePermission { Remote { @@ -26,12 +44,47 @@ pub enum RemotePermission { StatementSubmit, } +impl Display for RemotePermission { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Remote { domains } => { + if domains.is_empty() { + f.write_str("access to (no domains)") + } else { + let mut sorted: Vec<&str> = domains.iter().map(String::as_str).collect(); + sorted.sort(); + write!(f, "access to {}", sorted.join(", ")) + } + } + Self::WebRtc => f.write_str("WebRTC connections"), + Self::ChainSubmit => f.write_str("submit chain transactions"), + Self::PreimageSubmit => f.write_str("submit preimages"), + Self::StatementSubmit => f.write_str("submit statements"), + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct RemotePermissionRequest { /// Permissions requested by the product. pub permissions: Vec, } +impl Display for RemotePermissionRequest { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if self.permissions.is_empty() { + return f.write_str("(empty)"); + } + for (i, perm) in self.permissions.iter().enumerate() { + if i > 0 { + f.write_str("; ")?; + } + write!(f, "{perm}")?; + } + Ok(()) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct HostDevicePermissionResponse { /// Whether the permission was granted. @@ -43,3 +96,153 @@ pub struct RemotePermissionResponse { /// Whether the permission was granted. pub granted: bool, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn display_host_device_notifications() { + assert_eq!( + format!("{}", HostDevicePermissionRequest::Notifications), + "notifications" + ); + } + + #[test] + fn display_host_device_camera() { + assert_eq!(format!("{}", HostDevicePermissionRequest::Camera), "camera"); + } + + #[test] + fn display_host_device_microphone() { + assert_eq!( + format!("{}", HostDevicePermissionRequest::Microphone), + "microphone" + ); + } + + #[test] + fn display_host_device_bluetooth() { + assert_eq!( + format!("{}", HostDevicePermissionRequest::Bluetooth), + "bluetooth" + ); + } + + #[test] + fn display_host_device_nfc() { + assert_eq!(format!("{}", HostDevicePermissionRequest::NFC), "NFC"); + } + + #[test] + fn display_host_device_location() { + assert_eq!( + format!("{}", HostDevicePermissionRequest::Location), + "location" + ); + } + + #[test] + fn display_host_device_clipboard() { + assert_eq!( + format!("{}", HostDevicePermissionRequest::Clipboard), + "clipboard" + ); + } + + #[test] + fn display_host_device_open_url() { + assert_eq!( + format!("{}", HostDevicePermissionRequest::OpenUrl), + "open URL" + ); + } + + #[test] + fn display_host_device_biometrics() { + assert_eq!( + format!("{}", HostDevicePermissionRequest::Biometrics), + "biometrics" + ); + } + + #[test] + fn display_remote_permission_webrtc() { + assert_eq!( + format!("{}", RemotePermission::WebRtc), + "WebRTC connections" + ); + } + + #[test] + fn display_remote_permission_chain_submit() { + assert_eq!( + format!("{}", RemotePermission::ChainSubmit), + "submit chain transactions" + ); + } + + #[test] + fn display_remote_permission_preimage_submit() { + assert_eq!( + format!("{}", RemotePermission::PreimageSubmit), + "submit preimages" + ); + } + + #[test] + fn display_remote_permission_statement_submit() { + assert_eq!( + format!("{}", RemotePermission::StatementSubmit), + "submit statements" + ); + } + + #[test] + fn display_remote_permission_remote_empty_domains() { + let perm = RemotePermission::Remote { domains: vec![] }; + assert_eq!(format!("{perm}"), "access to (no domains)"); + } + + #[test] + fn display_remote_permission_remote_single_domain() { + let perm = RemotePermission::Remote { + domains: vec!["example.com".into()], + }; + assert_eq!(format!("{perm}"), "access to example.com"); + } + + #[test] + fn display_remote_permission_remote_multi_domain_sorted() { + let perm = RemotePermission::Remote { + domains: vec!["zeta.io".into(), "alpha.io".into(), "mid.io".into()], + }; + assert_eq!(format!("{perm}"), "access to alpha.io, mid.io, zeta.io"); + } + + #[test] + fn display_remote_permission_request_empty() { + let req = RemotePermissionRequest { + permissions: vec![], + }; + assert_eq!(format!("{req}"), "(empty)"); + } + + #[test] + fn display_remote_permission_request_multi_variant() { + let req = RemotePermissionRequest { + permissions: vec![ + RemotePermission::Remote { + domains: vec!["b.io".into(), "a.io".into()], + }, + RemotePermission::WebRtc, + RemotePermission::ChainSubmit, + ], + }; + assert_eq!( + format!("{req}"), + "access to a.io, b.io; WebRTC connections; submit chain transactions" + ); + } +} diff --git a/rust/crates/truapi/src/versioned/permissions.rs b/rust/crates/truapi/src/versioned/permissions.rs index 71558a5d..f620802c 100644 --- a/rust/crates/truapi/src/versioned/permissions.rs +++ b/rust/crates/truapi/src/versioned/permissions.rs @@ -1,6 +1,7 @@ //! Versioned wrappers for [`Permissions`](crate::api::Permissions) methods. use crate::v01; +use core::fmt::{self, Display, Formatter}; versioned_type! { pub enum HostDevicePermissionRequest { V1 => v01::HostDevicePermissionRequest } @@ -10,3 +11,45 @@ versioned_type! { pub enum RemotePermissionResponse { V1 => v01::RemotePermissionResponse } pub enum RemotePermissionError { V1 => v01::GenericError } } + +impl Display for HostDevicePermissionRequest { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::V1(inner) => write!(f, "{inner}"), + } + } +} + +impl Display for RemotePermissionRequest { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::V1(inner) => write!(f, "{inner}"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn host_device_permission_request_delegates_to_inner() { + let inner = v01::HostDevicePermissionRequest::Camera; + let versioned = HostDevicePermissionRequest::V1(inner); + assert_eq!(format!("{versioned}"), format!("{inner}")); + } + + #[test] + fn remote_permission_request_delegates_to_inner() { + let inner = v01::RemotePermissionRequest { + permissions: vec![ + v01::RemotePermission::WebRtc, + v01::RemotePermission::Remote { + domains: vec!["example.com".into()], + }, + ], + }; + let versioned = RemotePermissionRequest::V1(inner.clone()); + assert_eq!(format!("{versioned}"), format!("{inner}")); + } +} diff --git a/rust/crates/uniffi-bindgen-cli/Cargo.toml b/rust/crates/uniffi-bindgen-cli/Cargo.toml new file mode 100644 index 00000000..c0b5904f --- /dev/null +++ b/rust/crates/uniffi-bindgen-cli/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "uniffi-bindgen-cli" +version = "0.1.0" +edition = "2024" +description = "Thin CLI wrapper around uniffi::uniffi_bindgen_main() for the TrUAPI workspace" +license = "MIT" + +[[bin]] +name = "uniffi-bindgen" +path = "src/main.rs" + +[dependencies] +uniffi = { version = "0.29.4", features = ["cli"] } diff --git a/rust/crates/uniffi-bindgen-cli/README.md b/rust/crates/uniffi-bindgen-cli/README.md new file mode 100644 index 00000000..3ff1a773 --- /dev/null +++ b/rust/crates/uniffi-bindgen-cli/README.md @@ -0,0 +1,23 @@ +# uniffi-bindgen-cli + +Thin CLI wrapper around `uniffi::uniffi_bindgen_main()` for generating native bindings (Swift and Kotlin) from UniFFI inputs in this workspace. + +This crate exists so TrUAPI's native host SDKs (`host-libs/android`, `host-libs/ios`) can regenerate bindings via workspace-local tooling rather than relying on a globally installed `uniffi-bindgen`. + +It does not add custom logic. It forwards directly into UniFFI's standard CLI entry point. + +## Usage + +```bash +cargo run -p uniffi-bindgen-cli -- generate \ + --library target/debug/libtruapi_server.so \ + --language kotlin \ + --out-dir host-libs/android/src/generated + +cargo run -p uniffi-bindgen-cli -- generate \ + --library target/debug/libtruapi_server.dylib \ + --language swift \ + --out-dir host-libs/ios/TrUAPIHost/Sources/Generated +``` + +See `uniffi-bindgen --help` for the full CLI surface. diff --git a/rust/crates/uniffi-bindgen-cli/src/main.rs b/rust/crates/uniffi-bindgen-cli/src/main.rs new file mode 100644 index 00000000..fde8861e --- /dev/null +++ b/rust/crates/uniffi-bindgen-cli/src/main.rs @@ -0,0 +1,9 @@ +//! Thin CLI wrapper around `uniffi::uniffi_bindgen_main()`. +//! +//! Lets the TrUAPI workspace regenerate Kotlin and Swift bindings from +//! `truapi-server`'s UniFFI scaffolding without depending on a globally +//! installed `uniffi-bindgen`. + +fn main() { + uniffi::uniffi_bindgen_main(); +} diff --git a/scripts/codegen.sh b/scripts/codegen.sh index ef7e85f4..914d3c52 100755 --- a/scripts/codegen.sh +++ b/scripts/codegen.sh @@ -29,6 +29,10 @@ cargo run -p truapi-codegen -- \ --client-examples-output js/packages/truapi/test/generated/examples \ --host-output js/packages/truapi-host/src/generated \ --codec-version 1 +# TODO(phase-4): add --rust-output rust/crates/truapi-server/src/generated +# once the truapi-server crate lands. The Phase 1 emitter is already +# wired (see `truapi-codegen --rust-output`), but the output references +# `crate::dispatcher::Dispatcher` etc. that only exist inside truapi-server. npm exec --yes -- prettier --write \ "js/packages/truapi/src/generated/**/*.ts" \ From 3d3cc16c33041388e159e8a6bc0d3adba6c7327d Mon Sep 17 00:00:00 2001 From: pgherveou Date: Sun, 17 May 2026 08:43:30 +0000 Subject: [PATCH 02/49] fix: address review follow-ups #98, #99, #100, #102 - #98: SCALE-encode + hex the canonical remote permission bundle for the storage key instead of joining slugs with separators. Domains containing '|', ',', or 'truapi:permissions:' can no longer alias a different bundle's grant. - #99: golden_rust_emit tests use per-test tempdirs (no shared target/doc race) and panic loudly when nightly is unavailable instead of silently passing. - #100: strip phase/migration narration from doc comments and README; switch Chain trait stubs from CallError::HostFailure to CallError::Unsupported. - #102: replace ProtocolMessage::decode_error / call_error with free functions encode_decode_error / encode_call_error_payload returning Vec. Handlers now return Result, Vec>; the dispatcher owns envelope construction so a malformed empty-tag frame can no longer escape onto the wire. 3 new tests, 142 total. Workspace clean, clippy --all-features clean, wasm32 target clean. Relates to #96. --- rust/crates/truapi-codegen/src/main.rs | 3 +- rust/crates/truapi-codegen/src/rust.rs | 3 +- .../truapi-codegen/src/rust/dispatcher.rs | 23 +- .../truapi-codegen/tests/golden/dispatcher.rs | 200 +++++++++--------- .../truapi-codegen/tests/golden_rust_emit.rs | 140 +++++------- rust/crates/truapi-server/README.md | 5 - rust/crates/truapi-server/src/debug_log.rs | 1 - rust/crates/truapi-server/src/dispatcher.rs | 46 ++-- rust/crates/truapi-server/src/frame.rs | 70 +++--- .../truapi-server/src/generated/dispatcher.rs | 200 +++++++++--------- .../src/host_logic/permissions.rs | 114 +++++++--- rust/crates/truapi-server/src/lib.rs | 5 +- rust/crates/truapi-server/src/native.rs | 16 +- rust/crates/truapi-server/src/runtime.rs | 71 ++----- rust/crates/truapi-server/src/wasm.rs | 6 +- scripts/codegen.sh | 4 - 16 files changed, 452 insertions(+), 455 deletions(-) diff --git a/rust/crates/truapi-codegen/src/main.rs b/rust/crates/truapi-codegen/src/main.rs index d2c9c657..282e2156 100644 --- a/rust/crates/truapi-codegen/src/main.rs +++ b/rust/crates/truapi-codegen/src/main.rs @@ -55,8 +55,7 @@ struct Cli { /// Output directory for the generated Rust dispatcher / wire-table (optional). /// /// When set, emits `dispatcher.rs` and `wire_table.rs` for the - /// `truapi-server` crate to include. Phase 4 will wire this up to - /// `scripts/codegen.sh`; until then the flag is opt-in. + /// `truapi-server` crate to include. #[arg(long)] rust_output: Option, } diff --git a/rust/crates/truapi-codegen/src/rust.rs b/rust/crates/truapi-codegen/src/rust.rs index 064054f4..b58db059 100644 --- a/rust/crates/truapi-codegen/src/rust.rs +++ b/rust/crates/truapi-codegen/src/rust.rs @@ -2,8 +2,7 @@ //! //! Emits the server-side wire dispatcher (`dispatcher.rs`) and the //! discriminant lookup table (`wire_table.rs`). The generated files are -//! intended to be included in the `truapi-server` crate (Phase 4); the -//! codegen itself only diffs string output. +//! intended to be included in the `truapi-server` crate. use std::fs; use std::path::Path; diff --git a/rust/crates/truapi-codegen/src/rust/dispatcher.rs b/rust/crates/truapi-codegen/src/rust/dispatcher.rs index 35b82ab3..d382671a 100644 --- a/rust/crates/truapi-codegen/src/rust/dispatcher.rs +++ b/rust/crates/truapi-codegen/src/rust/dispatcher.rs @@ -9,9 +9,8 @@ //! 3. SCALE-encodes the versioned response wrapper back onto the wire. //! //! The generated file expects to live inside a `truapi-server` crate -//! and references `crate::dispatcher::Dispatcher`. The Phase 1 codegen -//! does not attempt to compile the output; only string-diff golden -//! tests guard it. +//! and references `crate::dispatcher::Dispatcher`. The codegen itself +//! does not compile the output; string-diff golden tests guard it. use std::collections::BTreeMap; use std::collections::BTreeSet; @@ -232,7 +231,7 @@ impl MethodEmission { .unwrap(); writeln!( out, - " Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?;" + " Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?;" ) .unwrap(); "&cx, request" @@ -255,7 +254,7 @@ impl MethodEmission { writeln!(out, " Ok(value) => value,").unwrap(); writeln!( out, - " Err(err) => return Err(ProtocolMessage::call_error(err))," + " Err(err) => return Err(encode_call_error_payload(err))," ) .unwrap(); writeln!(out, " }};").unwrap(); @@ -277,7 +276,7 @@ impl MethodEmission { writeln!(out, " Ok(()) => Ok(vec![0u8]),").unwrap(); writeln!( out, - " Err(err) => Err(ProtocolMessage::call_error(err))," + " Err(err) => Err(encode_call_error_payload(err))," ) .unwrap(); writeln!(out, " }}").unwrap(); @@ -316,7 +315,7 @@ impl MethodEmission { .unwrap(); writeln!( out, - " Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?;" + " Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?;" ) .unwrap(); writeln!( @@ -333,7 +332,7 @@ impl MethodEmission { writeln!(out, " Ok(sub) => sub,").unwrap(); writeln!( out, - " Err(err) => return Err(ProtocolMessage::call_error(err))," + " Err(err) => return Err(encode_call_error_payload(err))," ) .unwrap(); writeln!(out, " }};").unwrap(); @@ -360,7 +359,7 @@ impl MethodEmission { writeln!(out, " Ok(sub) => sub,").unwrap(); writeln!( out, - " Err(err) => return Err(ProtocolMessage::call_error(err))," + " Err(err) => return Err(encode_call_error_payload(err))," ) .unwrap(); writeln!(out, " }};").unwrap(); @@ -413,7 +412,11 @@ fn write_imports(out: &mut String, traits: &[&TraitDef]) { writeln!(out, "use truapi::versioned;").unwrap(); writeln!(out).unwrap(); writeln!(out, "use crate::dispatcher::Dispatcher;").unwrap(); - writeln!(out, "use crate::frame::ProtocolMessage;").unwrap(); + writeln!( + out, + "use crate::frame::{{encode_call_error_payload, encode_decode_error}};" + ) + .unwrap(); writeln!(out, "use crate::subscription::subscription_stream;").unwrap(); } diff --git a/rust/crates/truapi-codegen/tests/golden/dispatcher.rs b/rust/crates/truapi-codegen/tests/golden/dispatcher.rs index 8ae65341..fce2f8ff 100644 --- a/rust/crates/truapi-codegen/tests/golden/dispatcher.rs +++ b/rust/crates/truapi-codegen/tests/golden/dispatcher.rs @@ -26,7 +26,7 @@ use truapi::api::{ use truapi::versioned; use crate::dispatcher::Dispatcher; -use crate::frame::ProtocolMessage; +use crate::frame::{encode_call_error_payload, encode_decode_error}; use crate::subscription::subscription_stream; /// Register every TrUAPI method with the dispatcher. @@ -72,11 +72,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::account::HostAccountGetRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::account::HostAccountGetResponse = match host.get_account(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -91,11 +91,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::account::HostAccountGetAliasRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::account::HostAccountGetAliasResponse = match host.get_account_alias(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -110,11 +110,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::account::HostAccountCreateProofRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::account::HostAccountCreateProofResponse = match host.create_account_proof(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -129,11 +129,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::account::HostGetLegacyAccountsRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::account::HostGetLegacyAccountsResponse = match host.get_legacy_accounts(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -148,11 +148,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::account::HostGetUserIdRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::account::HostGetUserIdResponse = match host.get_user_id(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -167,11 +167,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::account::HostRequestLoginRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::account::HostRequestLoginResponse = match host.request_login(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -192,7 +192,7 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::chain::RemoteChainHeadFollowRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let stream = host.follow_head_subscribe(&cx, request).await; Ok(subscription_stream::(stream)) @@ -205,11 +205,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::chain::RemoteChainHeadHeaderRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainHeadHeaderResponse = match host.get_head_header(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -224,11 +224,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::chain::RemoteChainHeadBodyRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainHeadBodyResponse = match host.get_head_body(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -243,11 +243,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::chain::RemoteChainHeadStorageRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainHeadStorageResponse = match host.get_head_storage(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -262,11 +262,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::chain::RemoteChainHeadCallRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainHeadCallResponse = match host.call_head(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -281,11 +281,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::chain::RemoteChainHeadUnpinRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainHeadUnpinResponse = match host.unpin_head(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -300,11 +300,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::chain::RemoteChainHeadContinueRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainHeadContinueResponse = match host.continue_head(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -319,11 +319,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::chain::RemoteChainHeadStopOperationRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainHeadStopOperationResponse = match host.stop_head_operation(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -338,11 +338,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::chain::RemoteChainSpecGenesisHashRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainSpecGenesisHashResponse = match host.get_spec_genesis_hash(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -357,11 +357,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::chain::RemoteChainSpecChainNameRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainSpecChainNameResponse = match host.get_spec_chain_name(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -376,11 +376,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::chain::RemoteChainSpecPropertiesRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainSpecPropertiesResponse = match host.get_spec_properties(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -395,11 +395,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::chain::RemoteChainTransactionBroadcastRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainTransactionBroadcastResponse = match host.broadcast_transaction(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -414,11 +414,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::chain::RemoteChainTransactionStopRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainTransactionStopResponse = match host.stop_transaction(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -439,11 +439,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::chat::HostChatCreateRoomRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chat::HostChatCreateRoomResponse = match host.create_room(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -458,11 +458,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::chat::HostChatRegisterBotRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chat::HostChatRegisterBotResponse = match host.register_bot(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -489,11 +489,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::chat::HostChatPostMessageRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chat::HostChatPostMessageResponse = match host.post_message(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -520,7 +520,7 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::chat::ProductChatCustomMessageRenderSubscribeRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let stream = host.custom_message_render_subscribe(&cx, request).await; Ok(subscription_stream::(stream)) @@ -539,11 +539,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::entropy::HostDeriveEntropyRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::entropy::HostDeriveEntropyResponse = match host.derive(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -564,11 +564,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::jsonrpc::HostJsonrpcMessageSendRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::jsonrpc::HostJsonrpcMessageSendResponse = match host.send_message(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -583,7 +583,7 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::jsonrpc::HostJsonrpcMessageSubscribeRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let stream = host.subscribe_messages(&cx, request).await; Ok(subscription_stream::(stream)) @@ -602,11 +602,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::local_storage::HostLocalStorageReadRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::local_storage::HostLocalStorageReadResponse = match host.read(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -621,11 +621,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::local_storage::HostLocalStorageWriteRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::local_storage::HostLocalStorageWriteResponse = match host.write(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -640,11 +640,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::local_storage::HostLocalStorageClearRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::local_storage::HostLocalStorageClearResponse = match host.clear(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -665,11 +665,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::payment::HostPaymentBalanceSubscribeRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let stream = match host.balance_subscribe(&cx, request).await { Ok(sub) => sub, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; Ok(subscription_stream::(stream)) }) @@ -681,11 +681,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::payment::HostPaymentRequestRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::payment::HostPaymentRequestResponse = match host.request(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -700,11 +700,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::payment::HostPaymentStatusSubscribeRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let stream = match host.status_subscribe(&cx, request).await { Ok(sub) => sub, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; Ok(subscription_stream::(stream)) }) @@ -716,11 +716,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::payment::HostPaymentTopUpRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::payment::HostPaymentTopUpResponse = match host.top_up(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -741,11 +741,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::permissions::HostDevicePermissionRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::permissions::HostDevicePermissionResponse = match host.request_device_permission(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -760,11 +760,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::permissions::RemotePermissionRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::permissions::RemotePermissionResponse = match host.request_remote_permission(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -785,7 +785,7 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::preimage::RemotePreimageLookupSubscribeRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let stream = host.lookup_subscribe(&cx, request).await; Ok(subscription_stream::(stream)) @@ -798,11 +798,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::preimage::RemotePreimageSubmitRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::preimage::RemotePreimageSubmitResponse = match host.submit(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -823,11 +823,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::resource_allocation::HostRequestResourceAllocationRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::resource_allocation::HostRequestResourceAllocationResponse = match host.request(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -848,11 +848,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::signing::HostCreateTransactionRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::signing::HostCreateTransactionResponse = match host.create_transaction(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -867,11 +867,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::signing::HostCreateTransactionWithLegacyAccountRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::signing::HostCreateTransactionWithLegacyAccountResponse = match host.create_transaction_with_legacy_account(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -886,11 +886,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::signing::HostSignRawWithLegacyAccountRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::signing::HostSignRawWithLegacyAccountResponse = match host.sign_raw_with_legacy_account(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -905,11 +905,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::signing::HostSignPayloadWithLegacyAccountRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::signing::HostSignPayloadWithLegacyAccountResponse = match host.sign_payload_with_legacy_account(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -924,11 +924,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::signing::HostSignRawRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::signing::HostSignRawResponse = match host.sign_raw(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -943,11 +943,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::signing::HostSignPayloadRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::signing::HostSignPayloadResponse = match host.sign_payload(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -968,7 +968,7 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::statement_store::RemoteStatementStoreSubscribeRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let stream = host.subscribe(&cx, request).await; Ok(subscription_stream::(stream)) @@ -981,11 +981,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::statement_store::RemoteStatementStoreCreateProofRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::statement_store::RemoteStatementStoreCreateProofResponse = match host.create_proof(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -1000,11 +1000,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedResponse = match host.create_proof_authorized(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -1019,11 +1019,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::statement_store::RemoteStatementStoreSubmitRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); match host.submit(&cx, request).await { Ok(()) => Ok(vec![0u8]), - Err(err) => Err(ProtocolMessage::call_error(err)), + Err(err) => Err(encode_call_error_payload(err)), } }) }); @@ -1040,11 +1040,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::system::HostHandshakeRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::system::HostHandshakeResponse = match host.handshake(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -1059,11 +1059,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::system::HostFeatureSupportedRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::system::HostFeatureSupportedResponse = match host.feature_supported(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -1078,11 +1078,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::system::HostPushNotificationRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::system::HostPushNotificationResponse = match host.push_notification(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -1097,11 +1097,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::system::HostNavigateToRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::system::HostNavigateToResponse = match host.navigate_to(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); diff --git a/rust/crates/truapi-codegen/tests/golden_rust_emit.rs b/rust/crates/truapi-codegen/tests/golden_rust_emit.rs index 9a059842..9b72bff6 100644 --- a/rust/crates/truapi-codegen/tests/golden_rust_emit.rs +++ b/rust/crates/truapi-codegen/tests/golden_rust_emit.rs @@ -1,73 +1,62 @@ //! Golden snapshot test for the Rust dispatcher emitter. //! -//! The `ApiDefinition` model has no `Deserialize` impl (it is built by -//! the rustdoc extractor), so the "fixture" is a small JSON description -//! that this test deserializes into an `ApiDefinition` via a private -//! helper. The expected `dispatcher.rs` / `wire_table.rs` outputs live -//! under `tests/golden/`. +//! Each test runs `cargo +nightly rustdoc -p truapi` into its own +//! `--target-dir` under a per-test tempdir so concurrent test execution +//! cannot race on the shared `target/doc/truapi.json` path. Nightly Rust +//! is required; if it is not available the test panics rather than +//! silently passing (set up rustup with `rustup toolchain install nightly`). use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::Command; -/// Run the codegen binary directly. The binary accepts a rustdoc JSON -/// path; instead of generating that file at test time, this test boots -/// the codegen library via a small helper binary we ship alongside. -/// Easier: invoke the library's emitter via the codegen crate's -/// public modules. Since `truapi-codegen` is a `bin` crate, we use a -/// trick: include the source as a path with `#[path = "..."]`. -/// -/// Simpler approach: drive the test through the binary's CLI by -/// pre-generating a rustdoc JSON fixture. To avoid checking in a 12 MB -/// rustdoc dump, the test runs `cargo +nightly rustdoc -p truapi` -/// itself when the workspace toolchain has the nightly compiler. If -/// nightly isn't available the test prints a notice and exits. -#[test] -fn golden_dispatcher_and_wire_table() { - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let workspace_root = manifest_dir +/// Run `cargo +nightly rustdoc -p truapi --output-format json` into the +/// given `target_dir` and return the path to the produced JSON file. +/// Panics with a clear message if nightly is unavailable so CI cannot +/// pass vacuously. +fn produce_rustdoc_json(workspace_root: &Path, target_dir: &Path) -> PathBuf { + let output = Command::new("cargo") + .args(["+nightly", "rustdoc", "-p", "truapi", "--target-dir"]) + .arg(target_dir) + .args(["--", "-Z", "unstable-options", "--output-format", "json"]) + .current_dir(workspace_root) + .output() + .expect( + "failed to spawn `cargo +nightly rustdoc`; install nightly via \ + `rustup toolchain install nightly`", + ); + assert!( + output.status.success(), + "`cargo +nightly rustdoc` failed (status {}); nightly toolchain is required.\nstdout:\n{}\nstderr:\n{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + let json = target_dir.join("doc/truapi.json"); + assert!( + json.exists(), + "rustdoc JSON not found at {} after successful rustdoc invocation", + json.display(), + ); + json +} + +fn workspace_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) .ancestors() .nth(3) .expect("workspace root above rust/crates/truapi-codegen") - .to_path_buf(); - - // Generate rustdoc JSON for the truapi crate. This is what - // scripts/codegen.sh uses in production. Skip the test if nightly - // rustc isn't installed (the toolchain is required by `--output-format json`). - let rustdoc = Command::new("cargo") - .args([ - "+nightly", - "rustdoc", - "-p", - "truapi", - "--", - "-Z", - "unstable-options", - "--output-format", - "json", - ]) - .current_dir(&workspace_root) - .status(); - let rustdoc_status = match rustdoc { - Ok(status) => status, - Err(err) => { - eprintln!("skipping golden test: cargo +nightly rustdoc unavailable ({err})"); - return; - } - }; - if !rustdoc_status.success() { - eprintln!("skipping golden test: cargo +nightly rustdoc exited with {rustdoc_status}",); - return; - } + .to_path_buf() +} - let rustdoc_json = workspace_root.join("target/doc/truapi.json"); - assert!( - rustdoc_json.exists(), - "rustdoc JSON not found at {}", - rustdoc_json.display() - ); +#[test] +fn golden_dispatcher_and_wire_table() { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let workspace = workspace_root(); let tempdir = tempfile::tempdir().expect("tempdir"); + let rustdoc_json = produce_rustdoc_json(&workspace, &tempdir.path().join("rustdoc-target")); + let out = Command::new(env!("CARGO_BIN_EXE_truapi-codegen")) .args([ "--input", @@ -118,38 +107,9 @@ fn golden_dispatcher_and_wire_table() { /// inline unit tests might miss because they exercise smaller APIs. #[test] fn binary_emission_is_idempotent() { - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let workspace_root = manifest_dir - .ancestors() - .nth(3) - .expect("workspace root above rust/crates/truapi-codegen") - .to_path_buf(); - - // Reuse the rustdoc JSON if the golden test already produced it. - let rustdoc_json = workspace_root.join("target/doc/truapi.json"); - if !rustdoc_json.exists() { - let rustdoc = Command::new("cargo") - .args([ - "+nightly", - "rustdoc", - "-p", - "truapi", - "--", - "-Z", - "unstable-options", - "--output-format", - "json", - ]) - .current_dir(&workspace_root) - .status(); - match rustdoc { - Ok(s) if s.success() => {} - _ => { - eprintln!("skipping idempotence test: nightly rustdoc unavailable"); - return; - } - } - } + let workspace = workspace_root(); + let tempdir = tempfile::tempdir().expect("tempdir"); + let rustdoc_json = produce_rustdoc_json(&workspace, &tempdir.path().join("rustdoc-target")); let run_once = || -> (String, String) { let tmp = tempfile::tempdir().unwrap(); diff --git a/rust/crates/truapi-server/README.md b/rust/crates/truapi-server/README.md index e39bca78..4f30f9a0 100644 --- a/rust/crates/truapi-server/README.md +++ b/rust/crates/truapi-server/README.md @@ -14,11 +14,6 @@ - the auto-generated dispatcher/wire-table tables shipped under [`crate::generated`] -This crate is **Phase 4a** of the rust-core port (issue #96): only the -frame, dispatcher, subscription, transport and committed-codegen modules -are present. `core`, `runtime`, `host_logic`, chain, ws_bridge, native -and wasm bridges arrive in later waves. - ## Wire envelope Every frame on the wire is encoded as: diff --git a/rust/crates/truapi-server/src/debug_log.rs b/rust/crates/truapi-server/src/debug_log.rs index 0ac94da2..2e8aa878 100644 --- a/rust/crates/truapi-server/src/debug_log.rs +++ b/rust/crates/truapi-server/src/debug_log.rs @@ -32,7 +32,6 @@ pub fn emit(line: &str) { /// Wasm variant of `emit`: routes to the browser console. #[cfg(target_arch = "wasm32")] pub fn emit(line: &str) { - // Stub: full wasm bridge lands in Phase 4e along with web-sys. let _ = line; } diff --git a/rust/crates/truapi-server/src/dispatcher.rs b/rust/crates/truapi-server/src/dispatcher.rs index 2d646fec..b537f6b1 100644 --- a/rust/crates/truapi-server/src/dispatcher.rs +++ b/rust/crates/truapi-server/src/dispatcher.rs @@ -25,16 +25,19 @@ pub const LATEST_PROTOCOL_VERSION: u8 = 1; /// required to be `Send` because the truapi trait uses `async fn`, whose /// auto-Send-ness is not guaranteed. The `request_id` is the per-frame /// identifier; handlers thread it into the `CallContext` so trait methods -/// can correlate logs/cancellation with the originating request. -pub type RequestHandler = Arc< - dyn Fn(String, Vec) -> LocalBoxFuture<'static, Result, ProtocolMessage>> - + Send - + Sync, ->; +/// can correlate logs/cancellation with the originating request. On the +/// error path handlers return the SCALE-encoded `CallError` payload bytes +/// (typically via [`crate::frame::encode_decode_error`] or +/// [`crate::frame::encode_call_error_payload`]); the dispatcher wraps them +/// into the response envelope. +pub type RequestHandler = + Arc) -> LocalBoxFuture<'static, Result, Vec>> + Send + Sync>; -/// A handler for a subscription method. +/// A handler for a subscription method. On the error path the handler +/// returns the SCALE-encoded `CallError` payload bytes; the dispatcher +/// wraps them into an `_interrupt` envelope. pub type SubscriptionHandler = Arc< - dyn Fn(String, Vec) -> LocalBoxFuture<'static, Result> + dyn Fn(String, Vec) -> LocalBoxFuture<'static, Result>> + Send + Sync, >; @@ -81,7 +84,7 @@ impl Dispatcher { /// since each wire method must own exactly one handler. pub fn on_request(&mut self, method: &str, handler: F) -> Option where - F: Fn(String, Vec) -> LocalBoxFuture<'static, Result, ProtocolMessage>> + F: Fn(String, Vec) -> LocalBoxFuture<'static, Result, Vec>> + Send + Sync + 'static, @@ -94,10 +97,7 @@ impl Dispatcher { /// registered handler if any. pub fn on_subscription(&mut self, method: &str, handler: F) -> Option where - F: Fn( - String, - Vec, - ) -> LocalBoxFuture<'static, Result> + F: Fn(String, Vec) -> LocalBoxFuture<'static, Result>> + Send + Sync + 'static, @@ -120,19 +120,19 @@ impl Dispatcher { let result = handler(request_id, message.payload.value).await; // On the wire, every response is `Result`-shaped: // the handler returns `Ok(bytes)` already prefixed with a - // `0x00` discriminant for success, and `Err(ProtocolMessage)` - // whose payload is the SCALE-encoded `CallError`. The - // error path prepends `0x01` here so the wire payload is - // always `[disc][value...]`. + // `0x00` discriminant for success, and `Err(bytes)` whose + // bytes are the SCALE-encoded `CallError`. The error path + // prepends `0x01` here so the wire payload is always + // `[disc][value...]`. let payload = match result { Ok(value) => Payload { tag: compose_action(&method, FrameKind::Response), value, }, - Err(err_message) => { - let mut value = Vec::with_capacity(1 + err_message.payload.value.len()); + Err(err_bytes) => { + let mut value = Vec::with_capacity(1 + err_bytes.len()); value.push(1u8); - value.extend_from_slice(&err_message.payload.value); + value.extend_from_slice(&err_bytes); Payload { tag: compose_action(&method, FrameKind::Response), value, @@ -158,12 +158,12 @@ impl Dispatcher { transport, ); } - Err(err_message) => { + Err(err_bytes) => { transport.send(ProtocolMessage { request_id: message.request_id, payload: Payload { tag: compose_action(&method, FrameKind::Interrupt), - value: err_message.payload.value, + value: err_bytes, }, }); } @@ -255,7 +255,7 @@ mod tests { dispatcher.on_request("fake_method", |_request_id, _bytes| { Box::pin(async move { let err: CallError<()> = CallError::Denied; - Err(ProtocolMessage::call_error(err)) + Err(crate::frame::encode_call_error_payload(err)) }) }); let transport = Arc::new(RecordingTransport::default()); diff --git a/rust/crates/truapi-server/src/frame.rs b/rust/crates/truapi-server/src/frame.rs index 78ea3673..71328070 100644 --- a/rust/crates/truapi-server/src/frame.rs +++ b/rust/crates/truapi-server/src/frame.rs @@ -32,31 +32,20 @@ pub struct ProtocolMessage { pub payload: Payload, } -impl ProtocolMessage { - /// Build a placeholder error frame for a decode failure. The dispatcher - /// overlays `request_id` and the response tag before sending. - pub fn decode_error(reason: String) -> Self { - let err: CallError<()> = CallError::MalformedFrame { reason }; - Self { - request_id: String::new(), - payload: Payload { - tag: String::new(), - value: encode_call_error(&err), - }, - } - } +/// Encode `CallError::MalformedFrame { reason }` as the SCALE-encoded payload +/// bytes a handler returns on a decode failure. The dispatcher wraps these +/// bytes into a response frame with the matching `request_id` and response +/// tag. +pub fn encode_decode_error(reason: String) -> Vec { + let err: CallError<()> = CallError::MalformedFrame { reason }; + encode_call_error(&err) +} - /// Build a placeholder error frame for a host-side failure. The dispatcher - /// overlays `request_id` and the response tag before sending. - pub fn call_error(err: CallError) -> Self { - Self { - request_id: String::new(), - payload: Payload { - tag: String::new(), - value: encode_call_error(&err), - }, - } - } +/// Encode a `CallError` as the SCALE-encoded payload bytes a handler +/// returns on the error path. The dispatcher wraps these bytes into a +/// response frame with the matching `request_id` and response tag. +pub fn encode_call_error_payload(err: CallError) -> Vec { + encode_call_error(&err) } impl Encode for ProtocolMessage { @@ -518,4 +507,37 @@ mod tests { "decoding the 0xFF poison slot must fail", ); } + + #[test] + fn encode_decode_error_matches_malformed_frame_variant() { + let bytes = encode_decode_error("bad input".to_string()); + let mut expected = Vec::new(); + let err: CallError<()> = CallError::MalformedFrame { + reason: "bad input".to_string(), + }; + match &err { + CallError::MalformedFrame { reason } => { + 3u8.encode_to(&mut expected); + reason.encode_to(&mut expected); + } + _ => unreachable!(), + } + assert_eq!(bytes, expected); + } + + #[test] + fn encode_call_error_payload_matches_call_error_variants() { + let denied: CallError<()> = CallError::Denied; + assert_eq!(encode_call_error_payload(denied), vec![1u8]); + + let unsupported: CallError<()> = CallError::Unsupported; + assert_eq!(encode_call_error_payload(unsupported), vec![2u8]); + + let host: CallError<()> = CallError::HostFailure { + reason: "x".to_string(), + }; + let mut expected = vec![4u8]; + "x".to_string().encode_to(&mut expected); + assert_eq!(encode_call_error_payload(host), expected); + } } diff --git a/rust/crates/truapi-server/src/generated/dispatcher.rs b/rust/crates/truapi-server/src/generated/dispatcher.rs index 5660da55..cf6d0d51 100644 --- a/rust/crates/truapi-server/src/generated/dispatcher.rs +++ b/rust/crates/truapi-server/src/generated/dispatcher.rs @@ -14,7 +14,7 @@ use truapi::api::{ use truapi::versioned; use crate::dispatcher::Dispatcher; -use crate::frame::ProtocolMessage; +use crate::frame::{encode_call_error_payload, encode_decode_error}; use crate::subscription::subscription_stream; /// Register every TrUAPI method with the dispatcher. @@ -85,12 +85,12 @@ where Box::pin(async move { let request: versioned::account::HostAccountGetRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::account::HostAccountGetResponse = match host.get_account(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -109,12 +109,12 @@ where Box::pin(async move { let request: versioned::account::HostAccountGetAliasRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::account::HostAccountGetAliasResponse = match host.get_account_alias(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -133,12 +133,12 @@ where Box::pin(async move { let request: versioned::account::HostAccountCreateProofRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::account::HostAccountCreateProofResponse = match host.create_account_proof(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -157,12 +157,12 @@ where Box::pin(async move { let request: versioned::account::HostGetLegacyAccountsRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::account::HostGetLegacyAccountsResponse = match host.get_legacy_accounts(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -181,12 +181,12 @@ where Box::pin(async move { let request: versioned::account::HostGetUserIdRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::account::HostGetUserIdResponse = match host.get_user_id(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -205,12 +205,12 @@ where Box::pin(async move { let request: versioned::account::HostRequestLoginRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::account::HostRequestLoginResponse = match host.request_login(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -235,7 +235,7 @@ where Box::pin(async move { let request: versioned::chain::RemoteChainHeadFollowRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let stream = host.follow_head_subscribe(&cx, request).await; Ok(subscription_stream::< @@ -255,12 +255,12 @@ where Box::pin(async move { let request: versioned::chain::RemoteChainHeadHeaderRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainHeadHeaderResponse = match host.get_head_header(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -279,12 +279,12 @@ where Box::pin(async move { let request: versioned::chain::RemoteChainHeadBodyRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainHeadBodyResponse = match host.get_head_body(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -303,12 +303,12 @@ where Box::pin(async move { let request: versioned::chain::RemoteChainHeadStorageRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainHeadStorageResponse = match host.get_head_storage(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -327,12 +327,12 @@ where Box::pin(async move { let request: versioned::chain::RemoteChainHeadCallRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainHeadCallResponse = match host.call_head(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -351,12 +351,12 @@ where Box::pin(async move { let request: versioned::chain::RemoteChainHeadUnpinRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainHeadUnpinResponse = match host.unpin_head(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -375,12 +375,12 @@ where Box::pin(async move { let request: versioned::chain::RemoteChainHeadContinueRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainHeadContinueResponse = match host.continue_head(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -399,12 +399,12 @@ where Box::pin(async move { let request: versioned::chain::RemoteChainHeadStopOperationRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainHeadStopOperationResponse = match host.stop_head_operation(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -423,12 +423,12 @@ where Box::pin(async move { let request: versioned::chain::RemoteChainSpecGenesisHashRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainSpecGenesisHashResponse = match host.get_spec_genesis_hash(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -447,12 +447,12 @@ where Box::pin(async move { let request: versioned::chain::RemoteChainSpecChainNameRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainSpecChainNameResponse = match host.get_spec_chain_name(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -471,12 +471,12 @@ where Box::pin(async move { let request: versioned::chain::RemoteChainSpecPropertiesRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainSpecPropertiesResponse = match host.get_spec_properties(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -495,12 +495,12 @@ where Box::pin(async move { let request: versioned::chain::RemoteChainTransactionBroadcastRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainTransactionBroadcastResponse = match host.broadcast_transaction(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -519,12 +519,12 @@ where Box::pin(async move { let request: versioned::chain::RemoteChainTransactionStopRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chain::RemoteChainTransactionStopResponse = match host.stop_transaction(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -549,12 +549,12 @@ where Box::pin(async move { let request: versioned::chat::HostChatCreateRoomRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chat::HostChatCreateRoomResponse = match host.create_room(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -573,12 +573,12 @@ where Box::pin(async move { let request: versioned::chat::HostChatRegisterBotRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chat::HostChatRegisterBotResponse = match host.register_bot(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -615,12 +615,12 @@ where Box::pin(async move { let request: versioned::chat::HostChatPostMessageRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::chat::HostChatPostMessageResponse = match host.post_message(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -657,7 +657,7 @@ where Box::pin(async move { let request: versioned::chat::ProductChatCustomMessageRenderSubscribeRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let stream = host.custom_message_render_subscribe(&cx, request).await; Ok(subscription_stream::< @@ -683,12 +683,12 @@ where Box::pin(async move { let request: versioned::entropy::HostDeriveEntropyRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::entropy::HostDeriveEntropyResponse = match host.derive(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -713,12 +713,12 @@ where Box::pin(async move { let request: versioned::jsonrpc::HostJsonrpcMessageSendRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::jsonrpc::HostJsonrpcMessageSendResponse = match host.send_message(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -737,7 +737,7 @@ where Box::pin(async move { let request: versioned::jsonrpc::HostJsonrpcMessageSubscribeRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let stream = host.subscribe_messages(&cx, request).await; Ok(subscription_stream::< @@ -763,12 +763,12 @@ where Box::pin(async move { let request: versioned::local_storage::HostLocalStorageReadRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::local_storage::HostLocalStorageReadResponse = match host.read(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -787,12 +787,12 @@ where Box::pin(async move { let request: versioned::local_storage::HostLocalStorageWriteRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::local_storage::HostLocalStorageWriteResponse = match host.write(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -811,12 +811,12 @@ where Box::pin(async move { let request: versioned::local_storage::HostLocalStorageClearRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::local_storage::HostLocalStorageClearResponse = match host.clear(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -841,11 +841,11 @@ where Box::pin(async move { let request: versioned::payment::HostPaymentBalanceSubscribeRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let stream = match host.balance_subscribe(&cx, request).await { Ok(sub) => sub, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; Ok(subscription_stream::< versioned::payment::HostPaymentBalanceSubscribeItem, @@ -864,12 +864,12 @@ where Box::pin(async move { let request: versioned::payment::HostPaymentRequestRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::payment::HostPaymentRequestResponse = match host.request(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -888,11 +888,11 @@ where Box::pin(async move { let request: versioned::payment::HostPaymentStatusSubscribeRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let stream = match host.status_subscribe(&cx, request).await { Ok(sub) => sub, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; Ok(subscription_stream::< versioned::payment::HostPaymentStatusSubscribeItem, @@ -911,12 +911,12 @@ where Box::pin(async move { let request: versioned::payment::HostPaymentTopUpRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::payment::HostPaymentTopUpResponse = match host.top_up(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -941,12 +941,12 @@ where Box::pin(async move { let request: versioned::permissions::HostDevicePermissionRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::permissions::HostDevicePermissionResponse = match host.request_device_permission(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -965,12 +965,12 @@ where Box::pin(async move { let request: versioned::permissions::RemotePermissionRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::permissions::RemotePermissionResponse = match host.request_remote_permission(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -995,7 +995,7 @@ where Box::pin(async move { let request: versioned::preimage::RemotePreimageLookupSubscribeRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let stream = host.lookup_subscribe(&cx, request).await; Ok(subscription_stream::< @@ -1015,12 +1015,12 @@ where Box::pin(async move { let request: versioned::preimage::RemotePreimageSubmitRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::preimage::RemotePreimageSubmitResponse = match host.submit(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -1042,11 +1042,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::resource_allocation::HostRequestResourceAllocationRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::resource_allocation::HostRequestResourceAllocationResponse = match host.request(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -1070,12 +1070,12 @@ where Box::pin(async move { let request: versioned::signing::HostCreateTransactionRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::signing::HostCreateTransactionResponse = match host.create_transaction(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -1091,11 +1091,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::signing::HostCreateTransactionWithLegacyAccountRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::signing::HostCreateTransactionWithLegacyAccountResponse = match host.create_transaction_with_legacy_account(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -1113,12 +1113,12 @@ where Box::pin(async move { let request: versioned::signing::HostSignRawWithLegacyAccountRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::signing::HostSignRawWithLegacyAccountResponse = match host.sign_raw_with_legacy_account(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -1137,12 +1137,12 @@ where Box::pin(async move { let request: versioned::signing::HostSignPayloadWithLegacyAccountRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::signing::HostSignPayloadWithLegacyAccountResponse = match host.sign_payload_with_legacy_account(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -1161,12 +1161,12 @@ where Box::pin(async move { let request: versioned::signing::HostSignRawRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::signing::HostSignRawResponse = match host.sign_raw(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -1185,12 +1185,12 @@ where Box::pin(async move { let request: versioned::signing::HostSignPayloadRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::signing::HostSignPayloadResponse = match host.sign_payload(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -1215,7 +1215,7 @@ where Box::pin(async move { let request: versioned::statement_store::RemoteStatementStoreSubscribeRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let stream = host.subscribe(&cx, request).await; Ok(subscription_stream::< @@ -1232,11 +1232,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::statement_store::RemoteStatementStoreCreateProofRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::statement_store::RemoteStatementStoreCreateProofResponse = match host.create_proof(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -1251,11 +1251,11 @@ where let host = host.clone(); Box::pin(async move { let request: versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedRequest = - Decode::decode(&mut &bytes[..]).map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedResponse = match host.create_proof_authorized(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -1273,11 +1273,11 @@ where Box::pin(async move { let request: versioned::statement_store::RemoteStatementStoreSubmitRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); match host.submit(&cx, request).await { Ok(()) => Ok(vec![0u8]), - Err(err) => Err(ProtocolMessage::call_error(err)), + Err(err) => Err(encode_call_error_payload(err)), } }) }, @@ -1298,12 +1298,12 @@ where Box::pin(async move { let request: versioned::system::HostHandshakeRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::system::HostHandshakeResponse = match host.handshake(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -1322,12 +1322,12 @@ where Box::pin(async move { let request: versioned::system::HostFeatureSupportedRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::system::HostFeatureSupportedResponse = match host.feature_supported(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -1346,12 +1346,12 @@ where Box::pin(async move { let request: versioned::system::HostPushNotificationRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::system::HostPushNotificationResponse = match host.push_notification(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); @@ -1370,12 +1370,12 @@ where Box::pin(async move { let request: versioned::system::HostNavigateToRequest = Decode::decode(&mut &bytes[..]) - .map_err(|e| ProtocolMessage::decode_error(e.to_string()))?; + .map_err(|e| encode_decode_error(e.to_string()))?; let cx = CallContext::with_request_id(request_id.clone()); let response: versioned::system::HostNavigateToResponse = match host.navigate_to(&cx, request).await { Ok(value) => value, - Err(err) => return Err(ProtocolMessage::call_error(err)), + Err(err) => return Err(encode_call_error_payload(err)), }; let mut buf = Vec::with_capacity(1 + response.size_hint()); buf.push(0u8); diff --git a/rust/crates/truapi-server/src/host_logic/permissions.rs b/rust/crates/truapi-server/src/host_logic/permissions.rs index bc7d19c6..53ea0438 100644 --- a/rust/crates/truapi-server/src/host_logic/permissions.rs +++ b/rust/crates/truapi-server/src/host_logic/permissions.rs @@ -121,12 +121,32 @@ pub fn device_storage_key(permission: &HostDevicePermissionRequest) -> String { } /// Canonical storage key for a remote permission bundle. Permissions inside -/// the bundle are sorted so equivalent batches (same set, different order) -/// collapse to one storage entry. +/// the bundle are canonicalized (variants sorted, domain lists sorted) then +/// SCALE-encoded and hex-encoded so attacker-controlled domain strings cannot +/// collide with another bundle by injecting separator characters. pub fn remote_storage_key(request: &RemotePermissionRequest) -> String { - let mut slugs: Vec = request.permissions.iter().map(remote_slug).collect(); - slugs.sort(); - format!("{PERMISSION_KEY_PREFIX}remote:{}", slugs.join("|")) + let canonical = canonical_remote_bundle(request); + format!( + "{PERMISSION_KEY_PREFIX}remote:{}", + hex::encode(canonical.encode()) + ) +} + +fn canonical_remote_bundle(request: &RemotePermissionRequest) -> RemotePermissionRequest { + let mut permissions: Vec = request + .permissions + .iter() + .map(|p| match p { + RemotePermission::Remote { domains } => { + let mut sorted = domains.clone(); + sorted.sort(); + RemotePermission::Remote { domains: sorted } + } + other => other.clone(), + }) + .collect(); + permissions.sort_by_key(|p| p.encode()); + RemotePermissionRequest { permissions } } fn device_slug(permission: &HostDevicePermissionRequest) -> &'static str { @@ -143,20 +163,6 @@ fn device_slug(permission: &HostDevicePermissionRequest) -> &'static str { } } -fn remote_slug(permission: &RemotePermission) -> String { - match permission { - RemotePermission::Remote { domains } => { - let mut sorted = domains.clone(); - sorted.sort(); - format!("domains:{}", sorted.join(",")) - } - RemotePermission::WebRtc => "web-rtc".to_string(), - RemotePermission::ChainSubmit => "chain-submit".to_string(), - RemotePermission::PreimageSubmit => "preimage-submit".to_string(), - RemotePermission::StatementSubmit => "statement-submit".to_string(), - } -} - #[cfg(test)] mod tests { use super::*; @@ -246,19 +252,23 @@ mod tests { let chain = RemotePermissionRequest { permissions: vec![RemotePermission::ChainSubmit], }; - assert_eq!( - remote_storage_key(&chain), - "truapi:permissions:remote:chain-submit", + let expected = format!( + "truapi:permissions:remote:{}", + hex::encode(canonical_remote_bundle(&chain).encode()) ); - let domains = RemotePermissionRequest { + assert_eq!(remote_storage_key(&chain), expected); + + let unsorted = RemotePermissionRequest { permissions: vec![RemotePermission::Remote { domains: vec!["b.example.com".into(), "a.example.com".into()], }], }; - assert_eq!( - remote_storage_key(&domains), - "truapi:permissions:remote:domains:a.example.com,b.example.com", - ); + let sorted = RemotePermissionRequest { + permissions: vec![RemotePermission::Remote { + domains: vec!["a.example.com".into(), "b.example.com".into()], + }], + }; + assert_eq!(remote_storage_key(&unsorted), remote_storage_key(&sorted)); } #[test] @@ -272,6 +282,56 @@ mod tests { assert_eq!(remote_storage_key(&a), remote_storage_key(&b)); } + #[test] + fn remote_storage_key_handles_separator_chars_in_domains() { + // Domain strings containing `|`, `,`, or the `truapi:permissions:` + // prefix must not be able to forge a key that matches an unrelated + // bundle. We compare against a benign bundle with the same logical + // set of domains but no injection attempt. + let injecting = RemotePermissionRequest { + permissions: vec![RemotePermission::Remote { + domains: vec![ + "a|b".into(), + "c,d".into(), + "truapi:permissions:remote:web-rtc".into(), + ], + }], + }; + let benign_same_set = RemotePermissionRequest { + permissions: vec![RemotePermission::Remote { + domains: vec!["x".into(), "y".into(), "z".into()], + }], + }; + let injecting_key = remote_storage_key(&injecting); + let benign_key = remote_storage_key(&benign_same_set); + assert_ne!(injecting_key, benign_key); + + // The injecting bundle must also be distinct from a bundle that + // pretends to be `WebRtc + domains` via crafted strings. + let webrtc_plus_domains = RemotePermissionRequest { + permissions: vec![ + RemotePermission::WebRtc, + RemotePermission::Remote { + domains: vec!["a".into(), "b".into()], + }, + ], + }; + assert_ne!(injecting_key, remote_storage_key(&webrtc_plus_domains)); + + // Re-ordering the same domains in the injecting bundle still collapses + // to a single key (canonicalization is order-independent). + let injecting_reordered = RemotePermissionRequest { + permissions: vec![RemotePermission::Remote { + domains: vec![ + "truapi:permissions:remote:web-rtc".into(), + "c,d".into(), + "a|b".into(), + ], + }], + }; + assert_eq!(injecting_key, remote_storage_key(&injecting_reordered)); + } + #[test] fn check_or_prompt_device_caches_grant() { let storage = MemStorage::default(); diff --git a/rust/crates/truapi-server/src/lib.rs b/rust/crates/truapi-server/src/lib.rs index bd7d7fa5..25c1812e 100644 --- a/rust/crates/truapi-server/src/lib.rs +++ b/rust/crates/truapi-server/src/lib.rs @@ -1,11 +1,10 @@ //! TrUAPI server runtime: dispatcher, frames, SCALE encoding, stream management. //! -//! Phase 4c adds the runtime + host_logic + core layers on top of the -//! 4a skeleton. The platform path (`TrUApiCore::from_platform`) wraps a +//! The platform path (`TrUApiCore::from_platform`) wraps a //! [`truapi_platform::Platform`] in a `PlatformRuntimeHost` that implements //! every `truapi::api::*` trait by delegating to platform callbacks. //! -//! Phase 4e adds the host-facing bridges: +//! Host-facing bridges: //! - [`ws_bridge`] (feature `ws-bridge`): localhost WebSocket bridge for //! native WebView hosts (Android/iOS). //! - [`native`]: UniFFI surface exposing `NativeTrUApiCore` + `HostCallbacks`. diff --git a/rust/crates/truapi-server/src/native.rs b/rust/crates/truapi-server/src/native.rs index 74c792f2..78676dfb 100644 --- a/rust/crates/truapi-server/src/native.rs +++ b/rust/crates/truapi-server/src/native.rs @@ -6,11 +6,6 @@ //! resulting platform is fed into [`TrUApiCore::from_platform`] so the rest //! of the dispatcher pipeline behaves identically to the WS-bridge and wasm //! flavors. -//! -//! Note on adaptation: the bridge exposes `device_permission` and -//! `remote_permission` as separate callbacks (matching the v0.1 platform -//! surface) rather than the merged `prompt_permission` shape used by older -//! prototypes. `feature_supported` and `navigate_to` are retained. use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::Arc; @@ -424,10 +419,9 @@ impl Storage for CallbackPlatform { } } -// Account/signing/statement-store/preimage/chain capabilities aren't wired -// through the callback surface yet. The platform trait requires impls, so -// we stub them as "unavailable" responses or empty streams. Filling these -// in is a future phase when the corresponding native callbacks land. +// Account/signing/statement-store/preimage/chain capabilities are not wired +// through the callback surface. The platform trait requires impls, so we +// stub them as "unavailable" responses or empty streams. #[async_trait] impl ChainProvider for CallbackPlatform { @@ -436,7 +430,7 @@ impl ChainProvider for CallbackPlatform { _genesis_hash: truapi_platform::GenesisHash, ) -> Result, v01::GenericError> { Err(v01::GenericError::GenericError(v01::GenericErr { - reason: "chain provider not yet wired through native callbacks".into(), + reason: "chain provider not wired through native callbacks".into(), })) } } @@ -556,7 +550,7 @@ impl Preimage for CallbackPlatform { } fn unavailable_reason(method: &str) -> String { - format!("{method} unavailable on native host (callback not yet wired)") + format!("{method} unavailable on native host (callback not wired)") } struct NativeCallbackTransport { diff --git a/rust/crates/truapi-server/src/runtime.rs b/rust/crates/truapi-server/src/runtime.rs index 1c0abf32..5fc3e569 100644 --- a/rust/crates/truapi-server/src/runtime.rs +++ b/rust/crates/truapi-server/src/runtime.rs @@ -2,10 +2,9 @@ //! typed `truapi::api::*` host traits the generated dispatcher routes to. //! //! Most methods are straight delegations to the platform; the rest are -//! either stubbed out as `CallError::Unsupported` (because the v0.1 platform -//! surface does not yet cover them) or carry host-agnostic logic owned by -//! the core (e.g. `dotns` URL parsing for `navigate_to`, the permission -//! cache layer). +//! either stubbed out (as `CallError::Unsupported` for the Chain surface) +//! or carry host-agnostic logic owned by the core (e.g. `dotns` URL +//! parsing for `navigate_to`, the permission cache layer). use std::sync::Arc; @@ -335,7 +334,7 @@ where _request: HostRequestLoginRequest, ) -> Result> { Err(unsupported_with_reason( - "request_login is not part of the v0.1 platform surface", + "request_login is not implemented by the platform layer", )) } } @@ -370,7 +369,7 @@ where // create_transaction, create_transaction_with_legacy_account, // sign_payload_with_legacy_account, sign_raw_with_legacy_account fall - // back to the trait defaults (Err(CallError::unavailable())). The v0.1 + // back to the trait defaults (Err(CallError::unavailable())). The // platform surface only covers host_sign_payload / host_sign_raw. } @@ -418,8 +417,8 @@ where .map_err(|err| CallError::Domain(RemoteStatementStoreCreateProofError::V1(err))) } - // create_proof_authorized falls back to the default. The v0.1 platform - // surface does not expose pre-allocated allowance signing yet. + // create_proof_authorized falls back to the default. The platform + // surface does not expose pre-allocated allowance signing. } // --------------------------------------------------------------------------- @@ -441,7 +440,7 @@ where Subscription::new(stream) } - // submit falls back to the default. The platform surface does not yet + // submit falls back to the default. The platform surface does not // include preimage submission. } @@ -449,9 +448,9 @@ where // Chain // --------------------------------------------------------------------------- // -// Every method on the Chain trait is stubbed as Unsupported: the v0.1 -// platform surface exposes only the raw `ChainProvider::connect` JSON-RPC -// bridge. A chainHead state machine lives in a later phase (see Phase 4d). +// Every method on the Chain trait is stubbed as `CallError::Unsupported`. +// The platform exposes only the raw `ChainProvider::connect` JSON-RPC bridge; +// the typed chainHead surface is not implemented here. impl

Chain for PlatformRuntimeHost

where @@ -470,9 +469,7 @@ where _cx: &CallContext, _request: RemoteChainHeadHeaderRequest, ) -> Result> { - Err(unsupported_with_reason( - "chain runtime not yet provided by the platform layer", - )) + Err(CallError::Unsupported) } async fn get_head_body( @@ -480,9 +477,7 @@ where _cx: &CallContext, _request: RemoteChainHeadBodyRequest, ) -> Result> { - Err(unsupported_with_reason( - "chain runtime not yet provided by the platform layer", - )) + Err(CallError::Unsupported) } async fn get_head_storage( @@ -490,9 +485,7 @@ where _cx: &CallContext, _request: RemoteChainHeadStorageRequest, ) -> Result> { - Err(unsupported_with_reason( - "chain runtime not yet provided by the platform layer", - )) + Err(CallError::Unsupported) } async fn call_head( @@ -500,9 +493,7 @@ where _cx: &CallContext, _request: RemoteChainHeadCallRequest, ) -> Result> { - Err(unsupported_with_reason( - "chain runtime not yet provided by the platform layer", - )) + Err(CallError::Unsupported) } async fn unpin_head( @@ -510,9 +501,7 @@ where _cx: &CallContext, _request: RemoteChainHeadUnpinRequest, ) -> Result> { - Err(unsupported_with_reason( - "chain runtime not yet provided by the platform layer", - )) + Err(CallError::Unsupported) } async fn continue_head( @@ -520,9 +509,7 @@ where _cx: &CallContext, _request: RemoteChainHeadContinueRequest, ) -> Result> { - Err(unsupported_with_reason( - "chain runtime not yet provided by the platform layer", - )) + Err(CallError::Unsupported) } async fn stop_head_operation( @@ -531,9 +518,7 @@ where _request: RemoteChainHeadStopOperationRequest, ) -> Result> { - Err(unsupported_with_reason( - "chain runtime not yet provided by the platform layer", - )) + Err(CallError::Unsupported) } async fn get_spec_genesis_hash( @@ -542,9 +527,7 @@ where _request: RemoteChainSpecGenesisHashRequest, ) -> Result> { - Err(unsupported_with_reason( - "chain runtime not yet provided by the platform layer", - )) + Err(CallError::Unsupported) } async fn get_spec_chain_name( @@ -552,9 +535,7 @@ where _cx: &CallContext, _request: RemoteChainSpecChainNameRequest, ) -> Result> { - Err(unsupported_with_reason( - "chain runtime not yet provided by the platform layer", - )) + Err(CallError::Unsupported) } async fn get_spec_properties( @@ -562,9 +543,7 @@ where _cx: &CallContext, _request: RemoteChainSpecPropertiesRequest, ) -> Result> { - Err(unsupported_with_reason( - "chain runtime not yet provided by the platform layer", - )) + Err(CallError::Unsupported) } async fn broadcast_transaction( @@ -575,9 +554,7 @@ where RemoteChainTransactionBroadcastResponse, CallError, > { - Err(unsupported_with_reason( - "chain runtime not yet provided by the platform layer", - )) + Err(CallError::Unsupported) } async fn stop_transaction( @@ -586,9 +563,7 @@ where _request: RemoteChainTransactionStopRequest, ) -> Result> { - Err(unsupported_with_reason( - "chain runtime not yet provided by the platform layer", - )) + Err(CallError::Unsupported) } } diff --git a/rust/crates/truapi-server/src/wasm.rs b/rust/crates/truapi-server/src/wasm.rs index d2494365..babe2697 100644 --- a/rust/crates/truapi-server/src/wasm.rs +++ b/rust/crates/truapi-server/src/wasm.rs @@ -5,12 +5,8 @@ //! constructor. The bridge implements every host-side capability the //! [`truapi_platform::Platform`] trait set requires. Internally the bridge //! is wrapped in a [`SendWrapper`] so it satisfies the `Send` bound that -//! `async_trait`-generated futures inherit — sound on wasm32 because the +//! `async_trait`-generated futures inherit; sound on wasm32 because the //! runtime is single-threaded. -//! -//! Adaptation note: the JS bridge exposes `devicePermission` and -//! `remotePermission` as separate callbacks (matching the v0.1 split) and -//! retains `featureSupported` + `navigateTo`. use std::cell::Cell; use std::rc::Rc; diff --git a/scripts/codegen.sh b/scripts/codegen.sh index 914d3c52..ef7e85f4 100755 --- a/scripts/codegen.sh +++ b/scripts/codegen.sh @@ -29,10 +29,6 @@ cargo run -p truapi-codegen -- \ --client-examples-output js/packages/truapi/test/generated/examples \ --host-output js/packages/truapi-host/src/generated \ --codec-version 1 -# TODO(phase-4): add --rust-output rust/crates/truapi-server/src/generated -# once the truapi-server crate lands. The Phase 1 emitter is already -# wired (see `truapi-codegen --rust-output`), but the output references -# `crate::dispatcher::Dispatcher` etc. that only exist inside truapi-server. npm exec --yes -- prettier --write \ "js/packages/truapi/src/generated/**/*.ts" \ From 368db4cfe6d5be6b1ebf95d5e26c22d35d64913a Mon Sep 17 00:00:00 2001 From: pgherveou Date: Sun, 17 May 2026 08:52:53 +0000 Subject: [PATCH 03/49] fix(truapi-server): inject Spawner instead of std::thread::spawn per subscription (#97) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Subscriptions previously parked one OS thread each via std::thread::spawn(|| block_on(future)). On native this leaked a thread per active stream; on wasm32 it would panic outright since wasm has no threads. The dispatcher now requires a Spawner (Arc) + Send + Sync>) provided at construction time. Each entry point picks the right executor: - native.rs: shared futures::executor::ThreadPool (one pool per NativeTrUApiCore, default sized to CPU count). Falls back to the thread-per-subscription spawner if pool construction fails. - wasm.rs: wasm_bindgen_futures::spawn_local. - ws_bridge tests: thread_per_subscription_spawner() helper. TrUApiCore::new and ::from_platform now take the spawner. Dispatcher::new takes it too — no Default impl, since an empty Default would silently regress the leak. Added the `thread-pool` feature to the non-wasm32 `futures` dep. Added subscription_uses_provided_spawner_not_native_thread test. 143 tests passing, wasm32 target clean. Relates to #96. --- rust/crates/truapi-server/Cargo.toml | 1 + rust/crates/truapi-server/src/core.rs | 16 ++-- rust/crates/truapi-server/src/dispatcher.rs | 27 +++---- rust/crates/truapi-server/src/native.rs | 39 ++++++++- rust/crates/truapi-server/src/subscription.rs | 81 ++++++++++++++----- rust/crates/truapi-server/src/wasm.rs | 6 +- rust/crates/truapi-server/src/ws_bridge.rs | 5 +- .../truapi-server/tests/wire_result_shape.rs | 10 ++- 8 files changed, 139 insertions(+), 46 deletions(-) diff --git a/rust/crates/truapi-server/Cargo.toml b/rust/crates/truapi-server/Cargo.toml index 369027f5..d0690e28 100644 --- a/rust/crates/truapi-server/Cargo.toml +++ b/rust/crates/truapi-server/Cargo.toml @@ -28,6 +28,7 @@ hex = "0.4" tokio = { version = "1", features = ["rt-multi-thread", "net", "sync", "macros", "io-util"], optional = true } tokio-tungstenite = { version = "0.21", default-features = false, features = ["handshake"], optional = true } rand = { version = "0.8", optional = true } +futures = { version = "0.3", features = ["thread-pool"] } [target.'cfg(target_arch = "wasm32")'.dependencies] js-sys = "0.3" diff --git a/rust/crates/truapi-server/src/core.rs b/rust/crates/truapi-server/src/core.rs index 0115ba7f..038f3f01 100644 --- a/rust/crates/truapi-server/src/core.rs +++ b/rust/crates/truapi-server/src/core.rs @@ -21,6 +21,7 @@ use truapi_platform::Platform; use crate::generated::dispatcher; use crate::host_logic::session::SessionState; use crate::runtime::PlatformRuntimeHost; +use crate::subscription::Spawner; use crate::{Dispatcher, ProtocolMessage, Transport}; /// Top-level core. Owns the dispatcher and, on the platform path, the shared @@ -36,11 +37,12 @@ impl TrUApiCore { /// Build a core around a direct `TrUApi` implementation. The session /// state holder is unused on this path (no platform pushes updates), /// but is created anyway so the public API surface stays consistent. - pub fn new

(host: Arc

) -> Self + /// Subscription work runs on `spawner`. + pub fn new

(host: Arc

, spawner: Spawner) -> Self where P: TrUApi + 'static, { - let mut dispatcher = Dispatcher::new(); + let mut dispatcher = Dispatcher::new(spawner); dispatcher::register(&mut dispatcher, host); Self { dispatcher, @@ -50,13 +52,14 @@ impl TrUApiCore { /// Build a core around a [`Platform`] implementation. Wraps the platform /// in a [`PlatformRuntimeHost`] before registering with the dispatcher. - pub fn from_platform

(platform: Arc

) -> Self + /// Subscription work runs on `spawner`. + pub fn from_platform

(platform: Arc

, spawner: Spawner) -> Self where P: Platform + 'static, { let runtime = Arc::new(PlatformRuntimeHost::new(platform)); let session_state = runtime.session_state(); - let mut dispatcher = Dispatcher::new(); + let mut dispatcher = Dispatcher::new(spawner); dispatcher::register(&mut dispatcher, runtime); Self { dispatcher, @@ -340,7 +343,10 @@ mod tests { #[test] fn from_platform_dispatches_feature_supported() { - let core = TrUApiCore::from_platform(Arc::new(StubPlatform)); + let core = TrUApiCore::from_platform( + Arc::new(StubPlatform), + crate::subscription::thread_per_subscription_spawner(), + ); let request = HostFeatureSupportedRequest::V1(v01::HostFeatureSupportedRequest::Chain { genesis_hash: vec![0u8; 32], }); diff --git a/rust/crates/truapi-server/src/dispatcher.rs b/rust/crates/truapi-server/src/dispatcher.rs index b537f6b1..5e156411 100644 --- a/rust/crates/truapi-server/src/dispatcher.rs +++ b/rust/crates/truapi-server/src/dispatcher.rs @@ -13,7 +13,7 @@ use std::sync::atomic::AtomicU8; use futures::future::LocalBoxFuture; use crate::frame::{FrameKind, Payload, ProtocolMessage, compose_action}; -use crate::subscription::{SubscriptionManager, SubscriptionStream}; +use crate::subscription::{Spawner, SubscriptionManager, SubscriptionStream}; use crate::transport::Transport; /// Latest wire-codec version this server implements. Used as the default @@ -55,18 +55,19 @@ pub struct Dispatcher { impl Dispatcher { /// Construct a dispatcher with a fresh negotiated-version slot - /// (defaults to [`LATEST_PROTOCOL_VERSION`]). - pub fn new() -> Self { - Self::with_negotiated_version(Arc::new(AtomicU8::new(LATEST_PROTOCOL_VERSION))) + /// (defaults to [`LATEST_PROTOCOL_VERSION`]). Subscriptions are driven + /// on `spawner`. + pub fn new(spawner: Spawner) -> Self { + Self::with_negotiated_version(spawner, Arc::new(AtomicU8::new(LATEST_PROTOCOL_VERSION))) } /// Construct a dispatcher that shares its negotiated-version slot /// with another component (typically the host's handshake handler). - pub fn with_negotiated_version(negotiated_version: Arc) -> Self { + pub fn with_negotiated_version(spawner: Spawner, negotiated_version: Arc) -> Self { Self { request_handlers: HashMap::new(), subscription_handlers: HashMap::new(), - subscriptions: SubscriptionManager::new(), + subscriptions: SubscriptionManager::new(spawner), negotiated_version, } } @@ -179,12 +180,6 @@ impl Dispatcher { } } -impl Default for Dispatcher { - fn default() -> Self { - Self::new() - } -} - #[cfg(test)] mod tests { use super::*; @@ -232,7 +227,7 @@ mod tests { /// drop rather than panicking or sending a bogus response. #[test] fn dispatch_unknown_method_silently_drops() { - let dispatcher = Dispatcher::new(); + let dispatcher = Dispatcher::new(crate::subscription::thread_per_subscription_spawner()); let transport = Arc::new(RecordingTransport::default()); let transport_dyn: Arc = transport.clone(); let frame = make_frame( @@ -251,7 +246,8 @@ mod tests { /// discriminant byte (the Result wire shape). #[test] fn dispatch_request_handler_error_emits_response_with_err_discriminant() { - let mut dispatcher = Dispatcher::new(); + let mut dispatcher = + Dispatcher::new(crate::subscription::thread_per_subscription_spawner()); dispatcher.on_request("fake_method", |_request_id, _bytes| { Box::pin(async move { let err: CallError<()> = CallError::Denied; @@ -286,7 +282,8 @@ mod tests { /// returns the previous handler, so callers can detect collisions. #[test] fn register_request_twice_returns_previous_handler() { - let mut dispatcher = Dispatcher::new(); + let mut dispatcher = + Dispatcher::new(crate::subscription::thread_per_subscription_spawner()); let prev = dispatcher.on_request("fake_method", |_request_id, _bytes| { Box::pin(async move { Ok(Vec::new()) }) }); diff --git a/rust/crates/truapi-server/src/native.rs b/rust/crates/truapi-server/src/native.rs index 78676dfb..a3378bb0 100644 --- a/rust/crates/truapi-server/src/native.rs +++ b/rust/crates/truapi-server/src/native.rs @@ -12,7 +12,10 @@ use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use async_trait::async_trait; +use futures::executor::ThreadPool; +use futures::future::BoxFuture; use futures::stream::{self, BoxStream}; +use futures::task::SpawnExt; use parity_scale_codec::{Decode, Encode}; use truapi::v01; use truapi::versioned::account::{ @@ -38,6 +41,7 @@ use truapi_platform::{ Preimage, Signing, StatementStore, Storage, }; +use crate::subscription::Spawner; #[cfg(feature = "ws-bridge")] use crate::ws_bridge::{BridgeLogger, WsBridge, WsBridgeEndpoint, WsBridgeStartError}; use crate::{Payload, ProtocolMessage, TrUApiCore, Transport}; @@ -169,6 +173,11 @@ impl NativeTrUApiCore { /// over its [`HostCallbacks`] trait object; the core wraps it in a /// [`CallbackPlatform`] and feeds the result into /// [`TrUApiCore::from_platform`]. + /// + /// Subscriptions registered through this core run on a shared + /// `futures::executor::ThreadPool`. The pool sticks around for the + /// lifetime of the core; new subscriptions never spawn a fresh OS + /// thread each. #[uniffi::constructor] pub fn new(callbacks: Box) -> Arc { let callbacks: Arc = callbacks.into(); @@ -180,8 +189,9 @@ impl NativeTrUApiCore { let platform = Arc::new(CallbackPlatform { callbacks: callbacks.clone(), }); + let spawner = native_thread_pool_spawner(&callbacks); Arc::new(Self { - core: Arc::new(TrUApiCore::from_platform(platform)), + core: Arc::new(TrUApiCore::from_platform(platform, spawner)), callbacks, #[cfg(feature = "ws-bridge")] bridge: std::sync::Mutex::new(None), @@ -318,6 +328,33 @@ impl NativeTrUApiCore { } } +/// Build a [`Spawner`] backed by a shared `futures::executor::ThreadPool`. +/// The pool is sized at the default (one worker per logical CPU). Falls +/// back to a thread-per-subscription spawner if the pool fails to build, +/// which only ever happens if the host has no available threads at all. +fn native_thread_pool_spawner(callbacks: &Arc) -> Spawner { + match ThreadPool::new() { + Ok(pool) => { + let callbacks = callbacks.clone(); + Arc::new(move |fut: BoxFuture<'static, ()>| { + if let Err(err) = pool.spawn(fut) { + callbacks.on_core_log( + "truapi.native.core.subscription.spawn_failed".to_string(), + format!("{err}"), + ); + } + }) + } + Err(err) => { + callbacks.on_core_log( + "truapi.native.core.subscription.pool_unavailable".to_string(), + format!("{err}; falling back to thread-per-subscription"), + ); + crate::subscription::thread_per_subscription_spawner() + } + } +} + struct CallbackPlatform { callbacks: Arc, } diff --git a/rust/crates/truapi-server/src/subscription.rs b/rust/crates/truapi-server/src/subscription.rs index 6b5bf9a4..615cc67d 100644 --- a/rust/crates/truapi-server/src/subscription.rs +++ b/rust/crates/truapi-server/src/subscription.rs @@ -1,13 +1,15 @@ //! Subscription lifecycle management. //! //! Tracks active subscriptions (start/receive/stop/interrupt) and handles -//! cleanup when either side terminates. +//! cleanup when either side terminates. Each registered subscription drives +//! its stream on a caller-supplied [`Spawner`]; the manager itself never +//! creates threads or runtimes. use std::collections::HashMap; use std::sync::{Arc, Mutex}; use futures::StreamExt; -use futures::future::{Either, select}; +use futures::future::{BoxFuture, Either, select}; use futures::stream::BoxStream; use parity_scale_codec::Encode; @@ -16,13 +18,23 @@ use crate::transport::Transport; type StopFn = Box; -fn spawn_subscription(future: F) -where - F: std::future::Future + Send + 'static, -{ - std::thread::spawn(move || { - futures::executor::block_on(future); - }); +/// Spawns a subscription-driving future onto the caller's runtime. The +/// future is `Send` because the inner [`SubscriptionStream`] is a +/// `BoxStream<'static, _>` and every captured value the manager threads +/// through it is also `Send`. Each platform bridge supplies an +/// implementation that hands the future to the runtime driving its +/// transport (tokio `LocalSet`, `wasm_bindgen_futures::spawn_local`, ...). +pub type Spawner = Arc) + Send + Sync>; + +/// Convenience spawner for tests and embedders that don't yet wire a +/// real runtime: starts a fresh OS thread per subscription and drives the +/// future with `futures::executor::block_on`. Not available on wasm32 since +/// the platform has no threads. +#[cfg(not(target_arch = "wasm32"))] +pub fn thread_per_subscription_spawner() -> Spawner { + Arc::new(|fut: BoxFuture<'static, ()>| { + std::thread::spawn(move || futures::executor::block_on(fut)); + }) } /// One yielded value of a subscription stream after SCALE-encoding. @@ -54,13 +66,15 @@ where /// Manages active subscriptions on the server side. pub struct SubscriptionManager { active: Arc>>, + spawner: Spawner, } impl SubscriptionManager { - /// Create an empty manager. - pub fn new() -> Self { + /// Create an empty manager driven by `spawner`. + pub fn new(spawner: Spawner) -> Self { Self { active: Arc::new(Mutex::new(HashMap::new())), + spawner, } } @@ -95,7 +109,7 @@ impl SubscriptionManager { let active = self.active.clone(); - spawn_subscription(async move { + let future: BoxFuture<'static, ()> = Box::pin(async move { let completed = { let mut cancel_rx = cancel_rx; loop { @@ -145,6 +159,8 @@ impl SubscriptionManager { }); } }); + + (self.spawner)(future); } /// Handle a `_stop` frame from the product side. @@ -170,16 +186,11 @@ impl SubscriptionManager { } } -impl Default for SubscriptionManager { - fn default() -> Self { - Self::new() - } -} - #[cfg(test)] mod tests { use super::*; use futures::stream; + use std::sync::atomic::{AtomicUsize, Ordering}; /// Transport that records every frame and notifies waiters when it /// reaches a target count. Used to wait for the subscription's @@ -243,7 +254,7 @@ mod tests { fn register_then_stop_emits_no_extra_frames() { let transport_typed = Arc::new(RecordingTransport::new()); let transport_dyn: Arc = transport_typed.clone(); - let manager = SubscriptionManager::new(); + let manager = SubscriptionManager::new(thread_per_subscription_spawner()); let slow_stream: SubscriptionStream = Box::pin(stream::pending()); manager.register("p:1".to_string(), "demo_method", slow_stream, transport_dyn); manager.handle_stop("p:1"); @@ -261,7 +272,7 @@ mod tests { fn register_completion_emits_interrupt() { let transport_typed = Arc::new(RecordingTransport::new()); let transport_dyn: Arc = transport_typed.clone(); - let manager = SubscriptionManager::new(); + let manager = SubscriptionManager::new(thread_per_subscription_spawner()); let items = dummy_stream(vec![vec![0xaa], vec![0xbb]]); manager.register("p:1".to_string(), "demo_method", items, transport_dyn); let observed = transport_typed.wait_for(3, std::time::Duration::from_secs(2)); @@ -282,7 +293,7 @@ mod tests { fn double_stop_is_idempotent() { let transport_typed = Arc::new(RecordingTransport::new()); let transport_dyn: Arc = transport_typed.clone(); - let manager = SubscriptionManager::new(); + let manager = SubscriptionManager::new(thread_per_subscription_spawner()); let slow_stream: SubscriptionStream = Box::pin(stream::pending()); manager.register("p:1".to_string(), "demo_method", slow_stream, transport_dyn); manager.handle_stop("p:1"); @@ -294,4 +305,32 @@ mod tests { "double-stop must not emit any frame" ); } + + /// The manager must drive subscriptions through the injected spawner, + /// not by reaching out to `std::thread::spawn` itself. The counter + /// inside the test spawner is the proof. + #[test] + fn subscription_uses_provided_spawner_not_native_thread() { + let invocations = Arc::new(AtomicUsize::new(0)); + let invocations_for_spawner = invocations.clone(); + let spawner: Spawner = Arc::new(move |fut: BoxFuture<'static, ()>| { + invocations_for_spawner.fetch_add(1, Ordering::SeqCst); + std::thread::spawn(move || futures::executor::block_on(fut)); + }); + + let transport_typed = Arc::new(RecordingTransport::new()); + let transport_dyn: Arc = transport_typed.clone(); + let manager = SubscriptionManager::new(spawner); + let items = dummy_stream(vec![vec![0xcc]]); + manager.register("p:1".to_string(), "demo_method", items, transport_dyn); + + // Wait for the worker future to drain to completion so we know + // the spawner closure ran on this path. + let _ = transport_typed.wait_for(2, std::time::Duration::from_secs(2)); + assert_eq!( + invocations.load(Ordering::SeqCst), + 1, + "spawner must be invoked exactly once per register", + ); + } } diff --git a/rust/crates/truapi-server/src/wasm.rs b/rust/crates/truapi-server/src/wasm.rs index babe2697..3a3d42f5 100644 --- a/rust/crates/truapi-server/src/wasm.rs +++ b/rust/crates/truapi-server/src/wasm.rs @@ -47,6 +47,7 @@ use wasm_bindgen::prelude::*; use crate::TrUApiCore; use crate::frame::ProtocolMessage; +use crate::subscription::Spawner; use crate::transport::Transport; /// Bundle of JS-side callbacks the bridge invokes. Names map to camelCase @@ -714,7 +715,10 @@ impl WasmTrUApiCore { }); let dispose_fn = SendWrapper::new(bridge.dispose.clone()); let platform = Arc::new(WasmPlatform::new(bridge)); - let core = TrUApiCore::from_platform(platform); + let spawner: Spawner = Arc::new(|fut| { + wasm_bindgen_futures::spawn_local(fut); + }); + let core = TrUApiCore::from_platform(platform, spawner); Ok(Self { inner: Rc::new(WasmCoreInner { core, diff --git a/rust/crates/truapi-server/src/ws_bridge.rs b/rust/crates/truapi-server/src/ws_bridge.rs index 4973d2c2..9813554e 100644 --- a/rust/crates/truapi-server/src/ws_bridge.rs +++ b/rust/crates/truapi-server/src/ws_bridge.rs @@ -572,7 +572,10 @@ mod tests { /// the bridge echoes the SCALE-encoded `feature_supported` response. #[test] fn round_trip_feature_supported_through_bridge() { - let core = Arc::new(TrUApiCore::from_platform(Arc::new(StubPlatform))); + let core = Arc::new(TrUApiCore::from_platform( + Arc::new(StubPlatform), + crate::subscription::thread_per_subscription_spawner(), + )); let logger: BridgeLogger = Arc::new(|_, _| {}); let (mut bridge, endpoint) = WsBridge::start(0, core, logger).expect("start bridge"); let url = format!("ws://127.0.0.1:{}/?t={}", endpoint.port, endpoint.token); diff --git a/rust/crates/truapi-server/tests/wire_result_shape.rs b/rust/crates/truapi-server/tests/wire_result_shape.rs index f7acf693..7d1e0b6e 100644 --- a/rust/crates/truapi-server/tests/wire_result_shape.rs +++ b/rust/crates/truapi-server/tests/wire_result_shape.rs @@ -234,7 +234,10 @@ fn dispatch(core: &TrUApiCore, frame: ProtocolMessage) -> ProtocolMessage { #[test] fn feature_supported_ok_response_uses_ok_discriminant() { - let core = TrUApiCore::from_platform(Arc::new(StubPlatform)); + let core = TrUApiCore::from_platform( + Arc::new(StubPlatform), + truapi_server::subscription::thread_per_subscription_spawner(), + ); let request = HostFeatureSupportedRequest::V1(v01::HostFeatureSupportedRequest::Chain { genesis_hash: vec![0u8; 32], }); @@ -263,7 +266,10 @@ fn feature_supported_ok_response_uses_ok_discriminant() { #[test] fn local_storage_read_err_response_uses_err_discriminant() { - let core = TrUApiCore::from_platform(Arc::new(StubPlatform)); + let core = TrUApiCore::from_platform( + Arc::new(StubPlatform), + truapi_server::subscription::thread_per_subscription_spawner(), + ); let request = truapi::versioned::local_storage::HostLocalStorageReadRequest::V1( v01::HostLocalStorageReadRequest { key: "missing".to_string(), From 98d08f238de997dc768f0e7498382e736324c390 Mon Sep 17 00:00:00 2001 From: pgherveou Date: Sun, 17 May 2026 09:07:30 +0000 Subject: [PATCH 04/49] test(truapi-server,truapi-codegen): expand coverage (#101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit +33 tests across 7 files covering review gaps: Runtime delegation (core.rs, +13 tests): round-trip via TrUApiCore for get_account, get_account_alias, create_account_proof, get_legacy_accounts, get_user_id, sign_payload, sign_raw, LocalStorage read/write/clear, push_notification, request_remote_permission, connection_status_subscribe. Frame internals (frame.rs, +6): id_for_tag / tag_for_id known + unmapped, compose_action round-trip across every FrameKind, IdFactory monotonic and two-factory state isolation. WS bridge (ws_bridge.rs + native.rs, +3): wrong_token_is_rejected_at_handshake, drop_calls_stop_idempotently, start_ws_bridge_twice_returns_already_running. Session pruning (session.rs, +2): clear_when_empty_is_silent_no_op, dropped_subscriber_is_pruned. Permission error paths (permissions.rs, +3): prompt_failure_collapses_to_denial, corrupt_cache_entry_returns_none, storage_read_error_propagates. Codegen negative paths (truapi-codegen/src/rust.rs, +6): wire_table rejects request-with-subscription-id, subscription-with-request-id, missing IDs; dispatcher rejects multi-param methods and non-named-root response types. All via existing public APIs and test helpers — no production shims added. 176 total tests workspace-wide with ws-bridge feature. Relates to #96. --- rust/crates/truapi-codegen/src/rust.rs | 153 ++++++++++ rust/crates/truapi-server/src/core.rs | 275 +++++++++++++++++- rust/crates/truapi-server/src/frame.rs | 75 +++++ .../src/host_logic/permissions.rs | 106 +++++++ .../truapi-server/src/host_logic/session.rs | 42 +++ rust/crates/truapi-server/src/native.rs | 53 ++++ rust/crates/truapi-server/src/ws_bridge.rs | 57 ++++ 7 files changed, 760 insertions(+), 1 deletion(-) diff --git a/rust/crates/truapi-codegen/src/rust.rs b/rust/crates/truapi-codegen/src/rust.rs index b58db059..5fff464e 100644 --- a/rust/crates/truapi-codegen/src/rust.rs +++ b/rust/crates/truapi-codegen/src/rust.rs @@ -345,4 +345,157 @@ mod tests { ); assert_eq!(module_for_trait("Account"), "account"); } + + /// A request-kind method must not carry subscription wire ids. The + /// emitter rejects `start_id` / `stop_id` / `interrupt_id` / `receive_id` + /// on a `MethodKind::Request`. + #[test] + fn wire_table_request_with_subscription_id_errors() { + let mut method = make_request_method("alpha", 10); + method.wire.start_id = Some(99); + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Permissions".to_string(), + methods: vec![method], + docs: None, + }], + public_trait_order: vec!["Permissions".to_string()], + types: vec![], + }; + let err = generate_wire_table(&api).expect_err("request kind + start_id must error"); + let msg = format!("{err}"); + assert!( + msg.contains("must not use subscription wire ids"), + "unexpected error message: {msg}", + ); + } + + /// A subscription-kind method must not carry request wire ids. + #[test] + fn wire_table_subscription_with_request_id_errors() { + let mut method = make_subscription_method("connection_status_subscribe", 18); + method.wire.request_id = Some(99); + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Account".to_string(), + methods: vec![method], + docs: None, + }], + public_trait_order: vec!["Account".to_string()], + types: vec![], + }; + let err = generate_wire_table(&api).expect_err("subscription kind + request_id must error"); + let msg = format!("{err}"); + assert!( + msg.contains("must not use request wire ids"), + "unexpected error message: {msg}", + ); + } + + /// A request-kind method missing the mandatory `request_id` annotation + /// must fail emission, not silently default to 0. + #[test] + fn wire_table_missing_request_id_errors() { + let mut method = make_request_method("alpha", 10); + method.wire.request_id = None; + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Permissions".to_string(), + methods: vec![method], + docs: None, + }], + public_trait_order: vec!["Permissions".to_string()], + types: vec![], + }; + let err = generate_wire_table(&api).expect_err("missing request_id annotation must error"); + let msg = format!("{err}"); + assert!( + msg.contains("missing #[wire(request_id"), + "unexpected error message: {msg}", + ); + } + + /// Subscription-kind method missing `start_id` is similarly rejected. + #[test] + fn wire_table_missing_start_id_errors() { + let mut method = make_subscription_method("connection_status_subscribe", 18); + method.wire.start_id = None; + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Account".to_string(), + methods: vec![method], + docs: None, + }], + public_trait_order: vec!["Account".to_string()], + types: vec![], + }; + let err = generate_wire_table(&api).expect_err("missing start_id annotation must error"); + let msg = format!("{err}"); + assert!( + msg.contains("missing #[wire(start_id"), + "unexpected error message: {msg}", + ); + } + + /// The dispatcher expects each method to take exactly one versioned + /// wrapper parameter (plus `&self` and `&CallContext`, which are + /// elided from `params`). A method with two params errors out. + #[test] + fn dispatcher_multi_param_method_errors() { + let mut method = make_request_method("alpha", 10); + method.params.push(ParamDef { + name: "extra".to_string(), + type_ref: TypeRef::Named { + name: "ExtraWrapper".to_string(), + args: vec![], + }, + }); + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Permissions".to_string(), + methods: vec![method], + docs: None, + }], + public_trait_order: vec!["Permissions".to_string()], + types: vec![], + }; + let err = generate_dispatcher(&api).expect_err("two-param method must error"); + let msg = format!("{err}"); + assert!( + msg.contains("expected at most one request parameter"), + "unexpected error message: {msg}", + ); + } + + /// The response wrapper extraction expects a `TypeRef::Named` with no + /// generic args. Anything else (primitives, tuples, generics) errors. + #[test] + fn dispatcher_non_named_root_response_errors() { + let mut method = make_request_method("alpha", 10); + method.return_type = ReturnType::Result { + ok: TypeRef::Primitive("u32".to_string()), + err: TypeRef::Named { + name: "CallError".to_string(), + args: vec![TypeRef::Named { + name: "ErrWrapper".to_string(), + args: vec![], + }], + }, + }; + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Permissions".to_string(), + methods: vec![method], + docs: None, + }], + public_trait_order: vec!["Permissions".to_string()], + types: vec![], + }; + let err = generate_dispatcher(&api).expect_err("primitive response must error"); + let msg = format!("{err}"); + assert!( + msg.contains("response is not a versioned wrapper"), + "unexpected error message: {msg}", + ); + } } diff --git a/rust/crates/truapi-server/src/core.rs b/rust/crates/truapi-server/src/core.rs index 038f3f01..ca8982af 100644 --- a/rust/crates/truapi-server/src/core.rs +++ b/rust/crates/truapi-server/src/core.rs @@ -143,6 +143,10 @@ mod tests { HostAccountGetRequest, HostAccountGetResponse, HostGetLegacyAccountsRequest, HostGetLegacyAccountsResponse, HostGetUserIdRequest, HostGetUserIdResponse, }; + use truapi::versioned::local_storage::{ + HostLocalStorageClearRequest, HostLocalStorageReadRequest, HostLocalStorageWriteRequest, + }; + use truapi::versioned::permissions::RemotePermissionRequest; use truapi::versioned::preimage::{ RemotePreimageLookupSubscribeItem, RemotePreimageLookupSubscribeRequest, }; @@ -154,7 +158,9 @@ mod tests { RemoteStatementStoreSubmitRequest, RemoteStatementStoreSubscribeItem, RemoteStatementStoreSubscribeRequest, }; - use truapi::versioned::system::{HostFeatureSupportedRequest, HostFeatureSupportedResponse}; + use truapi::versioned::system::{ + HostFeatureSupportedRequest, HostFeatureSupportedResponse, HostPushNotificationRequest, + }; use truapi_platform::{ Accounts as PlatformAccounts, ChainProvider, Features, GenesisHash, JsonRpcConnection, Navigation, Notifications, Permissions, Preimage as PlatformPreimage, @@ -371,4 +377,271 @@ mod tests { // [Ok disc=0x00][V1 variant 0x00][supported=1] assert_eq!(response.payload.value, vec![0x00, 0x00, 0x01]); } + + /// Drive a request frame through `TrUApiCore::receive_from_product`, + /// decode the response envelope, and return its payload bytes (without + /// the wrapping ProtocolMessage). Shared by the runtime-delegation + /// tests below. + fn run_request(core: &TrUApiCore, method: &str, request_bytes: Vec) -> Vec { + let frame = ProtocolMessage { + request_id: "p:1".into(), + payload: Payload { + tag: compose_action(method, FrameKind::Request), + value: request_bytes, + }, + }; + let response_bytes = core + .receive_from_product(&frame.encode()) + .expect("dispatcher should emit a response"); + let response = ProtocolMessage::decode(&mut &response_bytes[..]).expect("decode response"); + assert_eq!(response.request_id, "p:1"); + assert_eq!( + response.payload.tag, + compose_action(method, FrameKind::Response), + ); + response.payload.value + } + + fn make_core() -> TrUApiCore { + TrUApiCore::from_platform( + Arc::new(StubPlatform), + crate::subscription::thread_per_subscription_spawner(), + ) + } + + fn product_id() -> v01::ProductAccountId { + v01::ProductAccountId { + dot_ns_identifier: "test.dot".into(), + derivation_index: 0, + } + } + + fn ring_location() -> v01::RingLocation { + v01::RingLocation { + genesis_hash: vec![0u8; 32], + ring_root_hash: vec![0u8; 32], + hints: None, + } + } + + #[test] + fn get_account_round_trips_stub_not_connected() { + let core = make_core(); + let request = HostAccountGetRequest::V1(v01::HostAccountGetRequest { + product_account_id: product_id(), + }); + let payload = run_request(&core, "account_get_account", request.encode()); + // Err disc 0x01, Domain disc 0x00, V1 variant 0x00, NotConnected variant 0x00. + assert_eq!(payload, vec![0x01, 0x00, 0x00, 0x00]); + } + + #[test] + fn get_account_alias_round_trips_stub_not_connected() { + let core = make_core(); + let request = HostAccountGetAliasRequest::V1(v01::HostAccountGetAliasRequest { + product_account_id: product_id(), + }); + let payload = run_request(&core, "account_get_account_alias", request.encode()); + // Err disc 0x01, Domain disc 0x00, V1 variant 0x00, NotConnected variant 0x00. + assert_eq!(payload, vec![0x01, 0x00, 0x00, 0x00]); + } + + #[test] + fn create_account_proof_round_trips_stub_ring_not_found() { + let core = make_core(); + let request = HostAccountCreateProofRequest::V1(v01::HostAccountCreateProofRequest { + product_account_id: product_id(), + ring_location: ring_location(), + context: vec![], + }); + let payload = run_request(&core, "account_create_account_proof", request.encode()); + // Err disc 0x01, Domain disc 0x00, V1 variant 0x00, RingNotFound variant 0x00. + assert_eq!(payload, vec![0x01, 0x00, 0x00, 0x00]); + } + + #[test] + fn get_legacy_accounts_round_trips_empty_list() { + let core = make_core(); + let request = HostGetLegacyAccountsRequest::V1; + let payload = run_request(&core, "account_get_legacy_accounts", request.encode()); + // Ok disc + decoded inner equals the stub response. + assert_eq!(payload[0], 0x00, "Ok disc"); + let expected = HostGetLegacyAccountsResponse::V1(v01::HostGetLegacyAccountsResponse { + accounts: vec![], + }); + let decoded = + HostGetLegacyAccountsResponse::decode(&mut &payload[1..]).expect("decode inner"); + assert_eq!(decoded, expected); + } + + #[test] + fn get_user_id_round_trips_stub_not_connected() { + let core = make_core(); + let request = HostGetUserIdRequest::V1; + let payload = run_request(&core, "account_get_user_id", request.encode()); + // Err disc 0x01, Domain disc 0x00, V1 variant 0x00, NotConnected variant. + // HostGetUserIdError variant order: PermissionDenied=0, NotConnected=1. + assert_eq!(payload, vec![0x01, 0x00, 0x00, 0x01]); + } + + #[test] + fn sign_payload_round_trips_stub_rejected() { + let core = make_core(); + let request = HostSignPayloadRequest::V1(v01::HostSignPayloadRequest { + account: product_id(), + block_hash: vec![], + block_number: vec![], + era: vec![], + genesis_hash: vec![], + method: vec![], + nonce: vec![], + spec_version: vec![], + tip: vec![], + transaction_version: vec![], + signed_extensions: vec![], + version: 4, + asset_id: None, + metadata_hash: None, + mode: None, + with_signed_transaction: None, + }); + let payload = run_request(&core, "signing_sign_payload", request.encode()); + // Err disc 0x01, Domain disc 0x00, V1 variant 0x00, Rejected variant. + // HostSignPayloadError: FailedToDecode=0, Rejected=1. + assert_eq!(payload, vec![0x01, 0x00, 0x00, 0x01]); + } + + #[test] + fn sign_raw_round_trips_stub_rejected() { + let core = make_core(); + let request = HostSignRawRequest::V1(v01::HostSignRawRequest { + account: product_id(), + payload: v01::RawPayload::Bytes { + bytes: vec![1, 2, 3], + }, + }); + let payload = run_request(&core, "signing_sign_raw", request.encode()); + // Same as sign_payload: HostSignPayloadError::Rejected discriminant. + assert_eq!(payload, vec![0x01, 0x00, 0x00, 0x01]); + } + + #[test] + fn local_storage_read_round_trips_none() { + let core = make_core(); + let request = HostLocalStorageReadRequest::V1(v01::HostLocalStorageReadRequest { + key: "missing".into(), + }); + let payload = run_request(&core, "local_storage_read", request.encode()); + // Ok disc 0x00, V1 variant 0x00, Option::None = 0x00. + assert_eq!(payload, vec![0x00, 0x00, 0x00]); + } + + #[test] + fn local_storage_write_round_trips_unit_ok() { + let core = make_core(); + let request = HostLocalStorageWriteRequest::V1(v01::HostLocalStorageWriteRequest { + key: "k".into(), + value: vec![1, 2, 3], + }); + let payload = run_request(&core, "local_storage_write", request.encode()); + // Ok disc 0x00, V1 variant 0x00. + assert_eq!(payload, vec![0x00, 0x00]); + } + + #[test] + fn local_storage_clear_round_trips_unit_ok() { + let core = make_core(); + let request = + HostLocalStorageClearRequest::V1(v01::HostLocalStorageClearRequest { key: "k".into() }); + let payload = run_request(&core, "local_storage_clear", request.encode()); + // Ok disc 0x00, V1 variant 0x00. + assert_eq!(payload, vec![0x00, 0x00]); + } + + #[test] + fn push_notification_round_trips_ok() { + let core = make_core(); + let request = HostPushNotificationRequest::V1(v01::HostPushNotificationRequest { + text: "hi".into(), + deeplink: None, + }); + let payload = run_request(&core, "system_push_notification", request.encode()); + // Stub returns Ok(()), so wire is Ok disc 0x00 + V1 variant 0x00. + assert_eq!(payload, vec![0x00, 0x00]); + } + + #[test] + fn request_remote_permission_round_trips_granted() { + let core = make_core(); + let request = RemotePermissionRequest::V1(v01::RemotePermissionRequest { + permissions: vec![v01::RemotePermission::ChainSubmit], + }); + let payload = run_request( + &core, + "permissions_request_remote_permission", + request.encode(), + ); + // Stub permissions grants every request. Wire is Ok disc 0x00, V1 + // variant 0x00, granted=1. + assert_eq!(payload, vec![0x00, 0x00, 0x01]); + } + + /// `connection_status_subscribe` produces a stream whose first item is + /// the current session state. Drive it through the dispatcher with a + /// recording transport and assert exactly one `_receive` frame appears. + #[test] + fn connection_status_subscribe_yields_initial_disconnected() { + use std::sync::Mutex; + + #[derive(Default)] + struct RecordingTransport { + sent: Mutex>, + } + impl Transport for RecordingTransport { + fn send(&self, message: ProtocolMessage) { + self.sent.lock().unwrap().push(message); + } + fn on_message( + &self, + _handler: Box, + ) -> Box { + Box::new(|| {}) + } + } + + let core = make_core(); + let transport = Arc::new(RecordingTransport::default()); + let dyn_transport: Arc = transport.clone(); + + let frame = ProtocolMessage { + request_id: "p:1".into(), + payload: Payload { + tag: compose_action("account_connection_status_subscribe", FrameKind::Start), + value: Vec::new(), + }, + }; + futures::executor::block_on(core.dispatch(frame, dyn_transport)); + + // Wait briefly for the spawned thread to emit the initial item. + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2); + loop { + if !transport.sent.lock().unwrap().is_empty() { + break; + } + if std::time::Instant::now() > deadline { + panic!("subscription did not yield an item in time"); + } + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + let sent = transport.sent.lock().unwrap().clone(); + assert!(!sent.is_empty(), "expected at least one _receive frame"); + let first = &sent[0]; + assert_eq!( + first.payload.tag, + compose_action("account_connection_status_subscribe", FrameKind::Receive,), + ); + // V1(Disconnected): V1 variant 0x00, Disconnected discriminant 0x00. + assert_eq!(first.payload.value, vec![0x00, 0x00]); + } } diff --git a/rust/crates/truapi-server/src/frame.rs b/rust/crates/truapi-server/src/frame.rs index 71328070..be924021 100644 --- a/rust/crates/truapi-server/src/frame.rs +++ b/rust/crates/truapi-server/src/frame.rs @@ -540,4 +540,79 @@ mod tests { "x".to_string().encode_to(&mut expected); assert_eq!(encode_call_error_payload(host), expected); } + + /// `id_for_tag` resolves a known tag (`system_feature_supported_request`, + /// id 2 per the generated table) without going through round-trip code. + #[test] + fn id_for_tag_known_method_returns_id() { + assert_eq!(id_for_tag("system_handshake_request"), Some(0)); + assert_eq!(id_for_tag("system_handshake_response"), Some(1)); + assert_eq!(id_for_tag("system_feature_supported_request"), Some(2)); + assert_eq!(id_for_tag("system_feature_supported_response"), Some(3)); + assert_eq!(id_for_tag("account_get_account_request"), Some(22)); + } + + /// `tag_for_id` maps a known id back to its tag, and the result is a + /// `&'static str` that compares equal to the same value the codec + /// composes. + #[test] + fn tag_for_id_known_id_returns_static_str() { + assert_eq!(tag_for_id(0), Some("system_handshake_request")); + assert_eq!(tag_for_id(2), Some("system_feature_supported_request")); + assert_eq!(tag_for_id(3), Some("system_feature_supported_response")); + // The leaked-tag cache hands out the same `&'static str` on + // repeated lookups for the same id. + assert!(std::ptr::eq(tag_for_id(2).unwrap(), tag_for_id(2).unwrap())); + } + + /// Unmapped slots return None; 0xFF is the documented poison slot and + /// every id past the populated range is also unmapped. + #[test] + fn tag_for_id_unmapped_id_returns_none() { + assert!(tag_for_id(0xFF).is_none()); + assert!(tag_for_id(250).is_none()); + } + + /// `compose_action` and `FrameKind::from_tag` must be exact inverses + /// for every `FrameKind` variant. This pins the suffix table. + #[test] + fn compose_action_round_trips_each_framekind() { + for kind in [ + FrameKind::Request, + FrameKind::Response, + FrameKind::Start, + FrameKind::Receive, + FrameKind::Stop, + FrameKind::Interrupt, + ] { + let tag = compose_action("system_feature_supported", kind); + let (method, parsed_kind) = + FrameKind::from_tag(&tag).expect("from_tag must parse the composed tag"); + assert_eq!(method, "system_feature_supported"); + assert_eq!(parsed_kind, kind, "round-trip mismatch for {kind:?}"); + assert_eq!(tag, format!("system_feature_supported_{}", kind.suffix())); + } + } + + /// IdFactory mints monotonically increasing ids prefixed with the + /// configured string. + #[test] + fn id_factory_minted_ids_are_unique_and_monotonic() { + let mut factory = IdFactory::new("p:"); + assert_eq!(factory.next_id(), "p:1"); + assert_eq!(factory.next_id(), "p:2"); + assert_eq!(factory.next_id(), "p:3"); + } + + /// Two distinct factories each maintain their own counter; minting from + /// one does not advance the other. + #[test] + fn two_factories_dont_share_state() { + let mut a = IdFactory::new("a:"); + let mut b = IdFactory::new("b:"); + assert_eq!(a.next_id(), "a:1"); + assert_eq!(b.next_id(), "b:1"); + assert_eq!(a.next_id(), "a:2"); + assert_eq!(b.next_id(), "b:2"); + } } diff --git a/rust/crates/truapi-server/src/host_logic/permissions.rs b/rust/crates/truapi-server/src/host_logic/permissions.rs index 53ea0438..06f4d663 100644 --- a/rust/crates/truapi-server/src/host_logic/permissions.rs +++ b/rust/crates/truapi-server/src/host_logic/permissions.rs @@ -444,4 +444,110 @@ mod tests { .unwrap(); assert_eq!(after, Some(Decision::Granted)); } + + /// Prompt callback failure collapses to `Denied` and is persisted. + /// Critically, the next `check_or_prompt_device` call must not + /// re-prompt; cached denials short-circuit just like cached grants. + struct FailingPrompt; + + #[async_trait] + impl Permissions for FailingPrompt { + async fn device_permission( + &self, + _request: HostDevicePermissionRequest, + ) -> Result { + Err(GenericError::GenericError(v01::GenericErr { + reason: "boom".into(), + })) + } + + async fn remote_permission( + &self, + _request: RemotePermissionRequest, + ) -> Result { + Err(GenericError::GenericError(v01::GenericErr { + reason: "boom".into(), + })) + } + } + + #[test] + fn prompt_failure_collapses_to_denial_and_persists() { + let storage = MemStorage::default(); + let prompt = FailingPrompt; + let service = PermissionsService::new(&storage, &prompt); + + let decision = futures::executor::block_on( + service.check_or_prompt_device(HostDevicePermissionRequest::Camera), + ) + .unwrap(); + assert_eq!(decision, Decision::Denied); + + // The denial is persisted; peek now returns Some(Denied). + let cached = + futures::executor::block_on(service.peek_device(&HostDevicePermissionRequest::Camera)) + .unwrap(); + assert_eq!(cached, Some(Decision::Denied)); + } + + /// A corrupt SCALE-encoded cache entry must be treated as "no cache", + /// not panic. The service falls back to prompting. + #[test] + fn corrupt_cache_entry_returns_none() { + let storage = MemStorage::default(); + // Write garbage bytes under the canonical key. + futures::executor::block_on(storage.write( + device_storage_key(&HostDevicePermissionRequest::Camera), + vec![0xff, 0xfe, 0xfd], + )) + .unwrap(); + + let prompt = ScriptedPrompt::new(vec![true], vec![]); + let service = PermissionsService::new(&storage, &prompt); + + let peeked = + futures::executor::block_on(service.peek_device(&HostDevicePermissionRequest::Camera)) + .unwrap(); + assert_eq!(peeked, None, "corrupt entry must decode as absent"); + } + + /// Storage failures must propagate to the caller; the service must not + /// swallow them by silently returning a default Decision. + #[derive(Default)] + struct FailingStorage; + + #[async_trait] + impl Storage for FailingStorage { + async fn read(&self, _key: StorageKey) -> Result, StorageError> { + Err(v01::HostLocalStorageReadError::Unknown { + reason: "read failed".into(), + }) + } + async fn write(&self, _key: StorageKey, _value: StorageValue) -> Result<(), StorageError> { + Err(v01::HostLocalStorageReadError::Unknown { + reason: "write failed".into(), + }) + } + async fn clear(&self, _key: StorageKey) -> Result<(), StorageError> { + Err(v01::HostLocalStorageReadError::Unknown { + reason: "clear failed".into(), + }) + } + } + + #[test] + fn storage_read_error_propagates() { + let storage = FailingStorage; + let prompt = ScriptedPrompt::new(vec![], vec![]); + let service = PermissionsService::new(&storage, &prompt); + + let err = futures::executor::block_on( + service.check_or_prompt_device(HostDevicePermissionRequest::Camera), + ) + .expect_err("read failure must surface"); + assert!(matches!( + err, + v01::HostLocalStorageReadError::Unknown { .. } + )); + } } diff --git a/rust/crates/truapi-server/src/host_logic/session.rs b/rust/crates/truapi-server/src/host_logic/session.rs index 142b1b9f..c969e52d 100644 --- a/rust/crates/truapi-server/src/host_logic/session.rs +++ b/rust/crates/truapi-server/src/host_logic/session.rs @@ -230,4 +230,46 @@ mod tests { VersionedItem::V1(HostAccountConnectionStatusSubscribeItem::Connected) ); } + + /// Clearing a never-set session is a no-op and must not synthesize a + /// spurious `Disconnected` event for live subscribers. + #[test] + fn clear_when_empty_is_silent_no_op() { + let state = SessionState::new(); + let mut stream = state.subscribe(); + // Drain the initial Disconnected. + let _ = block_on(stream.next()); + + state.clear_session(); + + let pending = stream.next().now_or_never(); + assert!(pending.is_none(), "no event expected when clear is a no-op",); + } + + /// Dropping a subscriber's stream must remove that sender from the + /// broadcast list. The next broadcast prunes it; the surviving stream + /// still receives the event. + #[test] + fn dropped_subscriber_is_pruned() { + let state = SessionState::new(); + let mut survivor = state.subscribe(); + let dropping = state.subscribe(); + let _ = block_on(survivor.next()); + // Drain the initial item from the dropping stream too so we don't + // accidentally test buffered-but-undelivered. + drop(dropping); + + state.set_session(info(0x33)); + let next = block_on(survivor.next()).expect("survivor must receive Connected"); + assert_eq!( + next, + VersionedItem::V1(HostAccountConnectionStatusSubscribeItem::Connected), + ); + + // Internally, `set_session`'s broadcast call `retain`-prunes any + // dropped senders. After the call the subscribers list should have + // exactly one entry (the survivor). + let inner = state.inner.lock().unwrap(); + assert_eq!(inner.subscribers.len(), 1, "dropped subscriber not pruned"); + } } diff --git a/rust/crates/truapi-server/src/native.rs b/rust/crates/truapi-server/src/native.rs index a3378bb0..b83e6aa2 100644 --- a/rust/crates/truapi-server/src/native.rs +++ b/rust/crates/truapi-server/src/native.rs @@ -734,4 +734,57 @@ mod tests { assert!(core.set_active_session(vec![0u8; 32], None, None)); core.clear_active_session(); } + + /// Calling `start_ws_bridge` twice on the same `NativeTrUApiCore` + /// without an intervening `stop_ws_bridge` is a hard error. The bridge + /// is single-instance per core, so the second start must surface + /// `AlreadyRunning` rather than silently leaking a worker thread. + #[cfg(feature = "ws-bridge")] + #[test] + fn start_ws_bridge_twice_returns_already_running() { + struct Noop; + impl HostCallbacks for Noop { + fn on_core_log(&self, _marker: String, _detail: String) {} + fn on_core_response(&self, _frame: Vec) {} + fn navigate_to(&self, _url: String) -> Result<(), HostNavigateRejection> { + Ok(()) + } + fn push_notification(&self, _payload: Vec) -> Result<(), HostRejection> { + Ok(()) + } + fn device_permission(&self, _request: Vec) -> Result { + Ok(false) + } + fn remote_permission(&self, _request: Vec) -> Result { + Ok(false) + } + fn feature_supported(&self, _request: Vec) -> Result { + Ok(false) + } + fn local_storage_read( + &self, + _key: String, + ) -> Result>, HostStorageError> { + Ok(None) + } + fn local_storage_write( + &self, + _key: String, + _value: Vec, + ) -> Result<(), HostStorageError> { + Ok(()) + } + fn local_storage_clear(&self, _key: String) -> Result<(), HostStorageError> { + Ok(()) + } + } + + let core = NativeTrUApiCore::new(Box::new(Noop)); + let _first = core.start_ws_bridge(0).expect("first start must succeed"); + let err = core + .start_ws_bridge(0) + .expect_err("second start must error"); + assert!(matches!(err, WsBridgeStartError::AlreadyRunning)); + core.stop_ws_bridge(); + } } diff --git a/rust/crates/truapi-server/src/ws_bridge.rs b/rust/crates/truapi-server/src/ws_bridge.rs index 9813554e..4eb79b6d 100644 --- a/rust/crates/truapi-server/src/ws_bridge.rs +++ b/rust/crates/truapi-server/src/ws_bridge.rs @@ -629,4 +629,61 @@ mod tests { bridge.stop(); } + + /// A handshake with the wrong `?t=` token must be rejected at the HTTP + /// upgrade step with a 401, not silently dropped. + #[test] + fn wrong_token_is_rejected_at_handshake() { + let core = Arc::new(TrUApiCore::from_platform( + Arc::new(StubPlatform), + crate::subscription::thread_per_subscription_spawner(), + )); + let logger: BridgeLogger = Arc::new(|_, _| {}); + let (mut bridge, endpoint) = WsBridge::start(0, core, logger).expect("start bridge"); + let url = format!("ws://127.0.0.1:{}/?t=bogus", endpoint.port); + + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("test runtime"); + + let err = rt + .block_on(async { tokio_tungstenite::connect_async(&url).await }) + .expect_err("connection must be refused"); + let msg = format!("{err}"); + assert!( + msg.contains("401") || msg.to_lowercase().contains("unauthorized"), + "expected 401/unauthorized rejection, got: {msg}", + ); + + bridge.stop(); + } + + /// Dropping a `WsBridge` handle without an explicit `stop()` must still + /// shut the worker thread down cleanly. `Drop::drop` calls `stop`, and + /// a second `stop` (from drop after the test's explicit one) is a + /// no-op. + #[test] + fn drop_calls_stop_idempotently() { + let core = Arc::new(TrUApiCore::from_platform( + Arc::new(StubPlatform), + crate::subscription::thread_per_subscription_spawner(), + )); + let logger: BridgeLogger = Arc::new(|_, _| {}); + let (bridge, _endpoint) = WsBridge::start(0, core, logger).expect("start bridge"); + // Drop the bridge; the worker thread must join via Drop. + drop(bridge); + + // Build a second bridge and explicitly stop twice. The second + // call has no shutdown sender and no thread handle left to join, + // so it returns without panicking. + let core = Arc::new(TrUApiCore::from_platform( + Arc::new(StubPlatform), + crate::subscription::thread_per_subscription_spawner(), + )); + let logger: BridgeLogger = Arc::new(|_, _| {}); + let (mut bridge, _endpoint) = WsBridge::start(0, core, logger).expect("start bridge"); + bridge.stop(); + bridge.stop(); + } } From 4814d9fd46a9ad7b26bb3f1365f35bb2e09f32b8 Mon Sep 17 00:00:00 2001 From: pgherveou Date: Sun, 17 May 2026 09:30:00 +0000 Subject: [PATCH 05/49] feat(truapi-server): chain runtime + smoldot provider (Phase 4d, #103) Lands chainHead-v1 state machine and light-client integration: - chain_runtime.rs (always compiled): ChainRuntime, RuntimeChainProvider, UnavailableChainProvider, json-rpc state machine + follow-event parsing into truapi::v01 RemoteChainHeadFollowItem variants. Spawner is threaded through to spawn the response loop, matching the dispatcher discipline. - smoldot_provider/ (feature `smoldot`, off by default): SmoldotChainProvider, SmoldotJsonRpcConnection wrapping smoldot-light. Native + wasm32 platform glue (websocket transport on wasm). Bundled paseo + asset-hub-paseo chainspecs. All 13 Chain trait methods are now routed through ChainRuntime instead of returning CallError::Unsupported. PlatformRuntimeHost wraps the host's ChainProvider into a RuntimeChainProvider via PlatformChainRuntimeProvider. Tests: 186 passing with --features ws-bridge,smoldot (+10 since previous). Smoldot adds ~36s to first compile; ~10s incremental. wasm32 target clean with --features smoldot. Closes #103 (Phase 4d portion). Relates to #96. --- Cargo.lock | 2155 ++++++++++++++++- rust/crates/truapi-server/Cargo.toml | 6 + .../crates/truapi-server/src/chain_runtime.rs | 1636 +++++++++++++ rust/crates/truapi-server/src/core.rs | 2 +- rust/crates/truapi-server/src/lib.rs | 4 + rust/crates/truapi-server/src/runtime.rs | 206 +- .../truapi-server/src/smoldot_provider/mod.rs | 309 +++ .../src/smoldot_provider/native_platform.rs | 16 + .../specs/asset-hub-paseo.json | 1 + .../src/smoldot_provider/specs/paseo.json | 1 + .../src/smoldot_provider/wasm_helpers.rs | 43 + .../src/smoldot_provider/wasm_platform.rs | 218 ++ .../src/smoldot_provider/wasm_socket.rs | 235 ++ 13 files changed, 4712 insertions(+), 120 deletions(-) create mode 100644 rust/crates/truapi-server/src/chain_runtime.rs create mode 100644 rust/crates/truapi-server/src/smoldot_provider/mod.rs create mode 100644 rust/crates/truapi-server/src/smoldot_provider/native_platform.rs create mode 100644 rust/crates/truapi-server/src/smoldot_provider/specs/asset-hub-paseo.json create mode 100644 rust/crates/truapi-server/src/smoldot_provider/specs/paseo.json create mode 100644 rust/crates/truapi-server/src/smoldot_provider/wasm_helpers.rs create mode 100644 rust/crates/truapi-server/src/smoldot_provider/wasm_platform.rs create mode 100644 rust/crates/truapi-server/src/smoldot_provider/wasm_socket.rs diff --git a/Cargo.lock b/Cargo.lock index 2ba3b9a8..cd56d4b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,43 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anstream" version = "1.0.0" @@ -38,7 +75,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -49,7 +86,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -58,6 +95,27 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" +dependencies = [ + "nodrop", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -106,6 +164,125 @@ dependencies = [ "winnow 0.7.15", ] +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -117,12 +294,42 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic-take" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8ab6b55fe97976e46f91ddbed8d147d966475dc29b2032757ba47e02376fbc3" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base32" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "basic-toml" version = "0.1.10" @@ -132,6 +339,24 @@ dependencies = [ "serde", ] +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +dependencies = [ + "hex-conservative", +] + [[package]] name = "bitflags" version = "2.11.1" @@ -150,6 +375,25 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2-rfc" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" +dependencies = [ + "arrayvec 0.4.12", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -159,11 +403,36 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +dependencies = [ + "allocator-api2", +] [[package]] name = "byte-slice-cast" @@ -215,12 +484,43 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.6.1" @@ -261,12 +561,36 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "colorchoice" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const_format" version = "0.2.36" @@ -281,89 +605,476 @@ dependencies = [ name = "const_format_proc_macros" version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-assembler-x64" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f81cede359311706057b689b91b59f464926de0316f389898a2b028cb494fa" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa6ca11305de425ea08884097b913ebe1a83875253b3c0063ce28411e226bfdc" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7537341a9a4ba9812141927be733e7254bf2318aab6597d567af9cad90609f27" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-bitset" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d28a4ca5faf25ff821fcc768f26e68ffef505e9f71bb06e608862d941fa65086" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-codegen" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d891057fe1b73910c41e73b32a70fa8454092fce65942b5fa6f72aa6d5487f8a" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.15.5", + "log", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", + "wasmtime-internal-math", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c29a66028a78eedc534b3a94e5ebfbaeb4e1f6b09038afe41bb24afd614faa4b" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95809ad251fe9422087b4a72d61e584d6ab6eff44dee1335f93cfaea0bedc9ac" + +[[package]] +name = "cranelift-control" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79d0cacf063c297e5e8d5b73cb355b41b87f6d248e252d1b284e7a7b73673c2" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2d73297a195ce3be55997c6307142c4b1e58dd0c2f18ceaa0179444024e312a" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-frontend" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be38d1ae29ef7c5d611fc6cb694f698dc4ca44152dcaa112ec0fef8d4d34858" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6761926f6636209de7ac568be28b206890f2181761375b9722e0a1e7a7e1637a" + +[[package]] +name = "cranelift-native" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0893472f73f0d530a28e9a573ada6d1f93b9659bb6734dfe17061ac967bd1830" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1daccebabb1ccd034dbab0eacc0722af27d3cccc7929dea27a3546cb3562e40" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-zebra" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775765289f7c6336c18d3d66127527820dd45ffd9eb3b6b8ee4708590e6c20f5" +dependencies = [ + "curve25519-dalek", + "ed25519", + "hashbrown 0.16.1", + "pkcs8", + "rand_core", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", + "libc", + "windows-sys 0.61.2", ] [[package]] -name = "convert_case" -version = "0.6.0" +name = "event-listener" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ - "unicode-segmentation", + "concurrent-queue", + "parking", + "pin-project-lite", ] [[package]] -name = "cpufeatures" -version = "0.2.17" +name = "event-listener-strategy" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "libc", + "event-listener", + "pin-project-lite", ] [[package]] -name = "crypto-common" -version = "0.1.7" +name = "fallible-iterator" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" [[package]] -name = "data-encoding" -version = "2.11.0" +name = "fallible-streaming-iterator" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] -name = "digest" -version = "0.10.7" +name = "fastbloom" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "ef975e30683b2d965054bb0a836f8973857c4ebf6acf274fe46617cd285060d8" dependencies = [ - "block-buffer", - "crypto-common", + "foldhash 0.2.0", + "libm", + "portable-atomic", + "siphasher 1.0.3", ] [[package]] -name = "displaydoc" -version = "0.2.5" +name = "fastrand" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] -name = "equivalent" -version = "1.0.2" +name = "fiat-crypto" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] -name = "errno" -version = "0.3.14" +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys", -] +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] -name = "fastrand" -version = "2.4.1" +name = "fnv" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" @@ -371,6 +1082,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -443,6 +1160,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.32" @@ -529,6 +1259,26 @@ dependencies = [ "wasip3", ] +[[package]] +name = "getrandom_or_panic" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1015b5a70616b688dc230cfe50c8af89d972cb132d5a622814d29773b10b9" +dependencies = [ + "rand_core", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +dependencies = [ + "fallible-iterator", + "indexmap", + "stable_deref_trait", +] + [[package]] name = "glob" version = "0.3.3" @@ -558,13 +1308,36 @@ dependencies = [ "scroll", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", + "serde", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", + "serde", + "serde_core", ] [[package]] @@ -573,18 +1346,72 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec 0.7.6", +] + +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array", + "hmac 0.8.1", +] + [[package]] name = "http" version = "1.4.0" @@ -742,12 +1569,30 @@ dependencies = [ "rustversion", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -766,6 +1611,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + [[package]] name = "konst" version = "0.2.20" @@ -793,6 +1647,71 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libsecp256k1" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79019718125edc905a079a70cfa5f3820bc76139fc91d6f9abc27ea2a887139" +dependencies = [ + "arrayref", + "base64", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand", + "serde", + "sha2 0.9.9", + "typenum", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -805,18 +1724,66 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix", +] + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core", + "zeroize", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -824,24 +1791,96 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] -name = "mio" -version = "1.2.0" +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multi-stash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685a9ac4b61f4e728e1d2c6a7844609c16527aeb5e6c865915c08e619c16410f" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "libc", - "wasi", - "windows-sys", + "autocfg", ] [[package]] -name = "nom" -version = "7.1.3" +name = "object" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ + "crc32fast", + "hashbrown 0.15.5", + "indexmap", "memchr", - "minimal-lexical", ] [[package]] @@ -856,13 +1895,19 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "parity-scale-codec" version = "3.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" dependencies = [ - "arrayvec", + "arrayvec 0.7.6", "bitvec", "byte-slice-cast", "const_format", @@ -884,6 +1929,53 @@ dependencies = [ "syn", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -916,12 +2008,82 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + [[package]] name = "plain" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -968,6 +2130,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulley-interpreter" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b78fdec962b639b921badfcfe77db7d18aa3c0c1e292ac2aa268c0efe8fe683" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-math", +] + +[[package]] +name = "pulley-macros" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f718f4e8cd5fdfa08b3b1d2d25fe288350051be330544305f0a9b93a937b3d42" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "quote" version = "1.0.45" @@ -1019,12 +2204,58 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regalloc2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5216b1837de2149f8bc8e6d5f88a9326b63b8c836ed58ce4a0a29ec736a59734" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.15.5", + "log", + "rustc-hash", + "smallvec", +] + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustc-hash" version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.4" @@ -1035,7 +2266,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1044,6 +2275,37 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ruzstd" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7c1c839d570d835527c9a5e4db7cb2198683a988cb9d7293fc8674e6bd58fc8" + +[[package]] +name = "schnorrkel" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9fcb6c2e176e86ec703e22560d99d65a5ee9056ae45a08e13e84ebf796296f" +dependencies = [ + "aead", + "arrayref", + "arrayvec 0.7.6", + "curve25519-dalek", + "getrandom_or_panic", + "merlin", + "rand_core", + "serde_bytes", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "scroll" version = "0.12.0" @@ -1099,6 +2361,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -1140,15 +2412,77 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" + [[package]] name = "siphasher" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + [[package]] name = "slab" version = "0.4.12" @@ -1160,6 +2494,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "smawk" @@ -1167,6 +2504,118 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +[[package]] +name = "smol" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", +] + +[[package]] +name = "smoldot" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b22238c3655a9e66285b5fc1a59aa914a3c09160caf5b9604f8c11de787d77f" +dependencies = [ + "arrayvec 0.7.6", + "async-lock", + "atomic-take", + "base32", + "base64", + "bip39", + "blake2-rfc", + "bs58", + "chacha20", + "crossbeam-queue", + "derive_more", + "ed25519-zebra", + "either", + "event-listener", + "fastbloom", + "fnv", + "futures-lite", + "futures-util", + "hashbrown 0.16.1", + "hex", + "hmac 0.12.1", + "itertools", + "libm", + "libsecp256k1", + "merlin", + "nom 8.0.0", + "num-bigint", + "num-rational", + "num-traits", + "parking_lot", + "pbkdf2", + "pin-project", + "poly1305", + "rand", + "rand_chacha", + "rusqlite", + "ruzstd", + "schnorrkel", + "serde", + "serde_json", + "sha2 0.10.9", + "sha3", + "siphasher 1.0.3", + "slab", + "smallvec", + "soketto", + "twox-hash", + "wasmi", + "wasmtime", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "smoldot-light" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ec52700b57df3f2a031873c5de7d44f2f8abae54b622ccf15ad9bc7c05a7c2" +dependencies = [ + "async-channel", + "async-lock", + "base64", + "blake2-rfc", + "bs58", + "derive_more", + "either", + "event-listener", + "fnv", + "futures-channel", + "futures-lite", + "futures-util", + "hashbrown 0.16.1", + "hex", + "itertools", + "log", + "lru", + "parking_lot", + "pin-project", + "rand", + "rand_chacha", + "serde", + "serde_json", + "siphasher 1.0.3", + "slab", + "smol", + "smoldot", + "zeroize", +] + [[package]] name = "socket2" version = "0.6.3" @@ -1174,7 +2623,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soketto" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" +dependencies = [ + "base64", + "bytes", + "futures", + "httparse", + "log", + "rand", + "sha1", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", ] [[package]] @@ -1195,6 +2675,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.117" @@ -1223,6 +2709,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + [[package]] name = "tempfile" version = "3.27.0" @@ -1233,7 +2725,16 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", ] [[package]] @@ -1322,7 +2823,7 @@ dependencies = [ "pin-project-lite", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1403,7 +2904,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", - "convert_case", + "convert_case 0.6.0", "indoc", "serde", "serde_json", @@ -1445,6 +2946,9 @@ dependencies = [ "pin-project", "rand", "send_wrapper 0.6.0", + "serde_json", + "smoldot", + "smoldot-light", "thiserror 1.0.69", "tokio", "tokio-tungstenite", @@ -1478,6 +2982,12 @@ dependencies = [ "utf-8", ] +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + [[package]] name = "typenum" version = "1.20.0" @@ -1609,7 +3119,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "beadc1f460eb2e209263c49c4f5b19e9a02e00a3b2b393f78ad10d766346ecff" dependencies = [ "anyhow", - "siphasher", + "siphasher 0.3.11", "uniffi_internal_macros", "uniffi_pipeline", ] @@ -1639,6 +3149,16 @@ dependencies = [ "weedle2", ] +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "url" version = "2.5.8" @@ -1669,6 +3189,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -1754,6 +3280,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.236.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "724fccfd4f3c24b7e589d333fc0429c68042897a7e8a5f8694f31792471841e7" +dependencies = [ + "leb128fmt", + "wasmparser 0.236.1", +] + [[package]] name = "wasm-encoder" version = "0.244.0" @@ -1761,7 +3297,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ "leb128fmt", - "wasmparser", + "wasmparser 0.244.0", ] [[package]] @@ -1772,8 +3308,71 @@ checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", "indexmap", - "wasm-encoder", - "wasmparser", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasmi" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19af97fcb96045dd1d6b4d23e2b4abdbbe81723dbc5c9f016eb52145b320063" +dependencies = [ + "arrayvec 0.7.6", + "multi-stash", + "smallvec", + "spin", + "wasmi_collections", + "wasmi_core", + "wasmi_ir", + "wasmparser 0.221.3", +] + +[[package]] +name = "wasmi_collections" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e80d6b275b1c922021939d561574bf376613493ae2b61c6963b15db0e8813562" + +[[package]] +name = "wasmi_core" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8c51482cc32d31c2c7ff211cd2bedd73c5bd057ba16a2ed0110e7a96097c33" +dependencies = [ + "downcast-rs", + "libm", +] + +[[package]] +name = "wasmi_ir" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e431a14c186db59212a88516788bd68ed51f87aa1e08d1df742522867b5289a" +dependencies = [ + "wasmi_core", +] + +[[package]] +name = "wasmparser" +version = "0.221.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06bfa36ab3ac2be0dee563380147a5b81ba10dd8885d7fbbc9eb574be67d185" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wasmparser" +version = "0.236.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9b1e81f3eb254cf7404a82cee6926a4a3ccc5aad80cc3d43608a070c67aa1d7" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", + "serde", ] [[package]] @@ -1788,6 +3387,253 @@ dependencies = [ "semver", ] +[[package]] +name = "wasmprinter" +version = "0.236.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2df225df06a6df15b46e3f73ca066ff92c2e023670969f7d50ce7d5e695abbb1" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.236.1", +] + +[[package]] +name = "wasmtime" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b10306ead921db2c4645ff99867b7539b65e18afd8816d471547f5e6f3b09492" +dependencies = [ + "addr2line", + "anyhow", + "async-trait", + "bitflags", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "hashbrown 0.15.5", + "indexmap", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rustix", + "semver", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasmparser 0.236.1", + "wasmtime-environ", + "wasmtime-internal-asm-macros", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-math", + "wasmtime-internal-slab", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wasmtime-internal-winch", + "windows-sys 0.60.2", +] + +[[package]] +name = "wasmtime-environ" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7fb2c37ca263d444f33871bf0221e7de0707b2b2bb88165df6db6d58c73375f" +dependencies = [ + "anyhow", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "indexmap", + "log", + "object", + "postcard", + "semver", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasm-encoder 0.236.1", + "wasmparser 0.236.1", + "wasmprinter", + "wasmtime-internal-component-util", +] + +[[package]] +name = "wasmtime-internal-asm-macros" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19c6c0d3c8d2db554a3af8e8d413ff2815362ebce0911808ecfdaaa257438f93" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e3f3752466eb0e1f97149e53bf15c0e18ff520fc0a98b4bee1680e6de1c6f0" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser 0.236.1", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f54018baf62f4e9c616c31f2aeadcf0c202ff691a390ad53e291ae7160b169e" + +[[package]] +name = "wasmtime-internal-cranelift" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a2412f2afb0a5db2a4ac1cfff73247e240aeaa90bf41497ad0a5084b6a24eca" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.236.1", + "wasmtime-environ", + "wasmtime-internal-math", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecfdc460dd5d343d88ff1ffaf65ae019feeb6124ddcfd3f39d28331068d25b1f" +dependencies = [ + "anyhow", + "cc", + "cfg-if", + "libc", + "rustix", + "wasmtime-internal-asm-macros", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.60.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5abb428a71827b7f90fc64406749883ccc6e58addf6d36974d5e06942011707" +dependencies = [ + "cc", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6cc13f14c3fb83fb877cb1d5c605e93f7ec1bf7fc1a5e8b361209d2f8ca028" +dependencies = [ + "anyhow", + "cfg-if", + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "wasmtime-internal-math" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cb209473a09f4dbd9c87bb9f18b8dcb0c9da30d12a260e3eacf7a1a53b41480" +dependencies = [ + "libm", +] + +[[package]] +name = "wasmtime-internal-slab" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aab4df5a04752106e1ecef9d40145ef28fa033b0d5dd3c839c9b208b2d522183" + +[[package]] +name = "wasmtime-internal-unwinder" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5359875d29bddb6f7e65e698157714d8d35ebd8ea2a92893d05d6b062147b639" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "log", + "object", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e247bcdd69701743ba386c933b26ebad2ce912ff9cb68b5b71fdb29d39ba04a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasmtime-internal-winch" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0298dfd9f57588222b5a92dcffe75894f1ead4e519850f176bde7fcfd105d54" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli", + "object", + "target-lexicon", + "wasmparser 0.236.1", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "winch-codegen", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1706803e83b9bae726a0f55e7c1bbf78a7421cf2da68c940c70978e91dfc0339" +dependencies = [ + "anyhow", + "bitflags", + "heck", + "indexmap", + "wit-parser 0.236.1", +] + [[package]] name = "web-sys" version = "0.3.98" @@ -1814,7 +3660,36 @@ version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" dependencies = [ - "nom", + "nom 7.1.3", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winch-codegen" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e2d7ea2137be52644d9c42ca5a4899bba07c2ed2db1e66c4c1994adfe35d39e" +dependencies = [ + "anyhow", + "cranelift-assembler-x64", + "cranelift-codegen", + "gimli", + "regalloc2", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.236.1", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "wasmtime-internal-math", ] [[package]] @@ -1823,6 +3698,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1832,6 +3716,71 @@ dependencies = [ "windows-link", ] +[[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", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "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.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.15" @@ -1873,7 +3822,7 @@ checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", "heck", - "wit-parser", + "wit-parser 0.244.0", ] [[package]] @@ -1920,10 +3869,28 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder", + "wasm-encoder 0.244.0", "wasm-metadata", - "wasmparser", - "wit-parser", + "wasmparser 0.244.0", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-parser" +version = "0.236.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e4833a20cd6e85d6abfea0e63a399472d6f88c6262957c17f546879a80ba15" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.236.1", ] [[package]] @@ -1941,7 +3908,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser", + "wasmparser 0.244.0", ] [[package]] @@ -1959,6 +3926,18 @@ dependencies = [ "tap", ] +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + [[package]] name = "yoke" version = "0.8.2" @@ -2023,6 +4002,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerotrie" version = "0.2.4" diff --git a/rust/crates/truapi-server/Cargo.toml b/rust/crates/truapi-server/Cargo.toml index d0690e28..eb268583 100644 --- a/rust/crates/truapi-server/Cargo.toml +++ b/rust/crates/truapi-server/Cargo.toml @@ -11,6 +11,7 @@ crate-type = ["rlib", "cdylib", "staticlib"] [features] default = [] ws-bridge = ["dep:tokio", "dep:tokio-tungstenite", "dep:rand"] +smoldot = ["dep:smoldot-light", "dep:smoldot"] [dependencies] truapi = { path = "../truapi" } @@ -18,6 +19,7 @@ truapi-platform = { path = "../truapi-platform" } async-trait = "0.1" futures = "0.3" parity-scale-codec = { version = "3", features = ["derive"] } +serde_json = "1" thiserror = "1" unicode-normalization = "0.1" url = "2" @@ -25,6 +27,8 @@ uniffi = "0.29.4" hex = "0.4" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +smoldot-light = { version = "1.1.0", optional = true } +smoldot = { version = "1.1.0", optional = true } tokio = { version = "1", features = ["rt-multi-thread", "net", "sync", "macros", "io-util"], optional = true } tokio-tungstenite = { version = "0.21", default-features = false, features = ["handshake"], optional = true } rand = { version = "0.8", optional = true } @@ -34,6 +38,8 @@ futures = { version = "0.3", features = ["thread-pool"] } js-sys = "0.3" wasm-bindgen = "0.2.118" wasm-bindgen-futures = "0.4" +smoldot-light = { version = "1.1.0", optional = true, default-features = false } +smoldot = { version = "1.1.0", optional = true, default-features = false, features = ["std"] } futures-timer = { version = "3", features = ["wasm-bindgen"] } futures-util = "0.3" getrandom = { version = "0.2", features = ["js"] } diff --git a/rust/crates/truapi-server/src/chain_runtime.rs b/rust/crates/truapi-server/src/chain_runtime.rs new file mode 100644 index 00000000..a22836e2 --- /dev/null +++ b/rust/crates/truapi-server/src/chain_runtime.rs @@ -0,0 +1,1636 @@ +//! ChainHead v1 state machine used by `PlatformRuntimeHost`. +//! +//! [`ChainRuntime`] keeps one [`ChainConnection`] per chain (keyed by genesis +//! hash) on top of the platform-provided [`JsonRpcConnection`]. Each connection +//! owns the per-product `chainHead_v1_follow` subscriptions, the in-flight +//! request map, and the json-rpc response loop. The follow event stream is +//! parsed into v01 [`RemoteChainHeadFollowItem`] values; one-shot calls +//! (header / body / call / storage / spec / broadcast / stop) are submitted as +//! json-rpc requests and the matching response is decoded back into a typed +//! v01 result. +//! +//! The chain-side traits return [`RuntimeFailure`], a local classification +//! that the [`crate::runtime`] layer maps to [`truapi::CallError`] variants +//! (`Unsupported`, `HostFailure`, ...). This avoids leaking json-rpc plumbing +//! into the public API. + +use std::collections::HashMap; +use std::pin::Pin; +use std::sync::Arc; +use std::sync::Mutex; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::task::{Context, Poll}; + +use futures::FutureExt; +use futures::channel::{mpsc, oneshot}; +use futures::stream::BoxStream; +use futures::{Stream, StreamExt}; +use serde_json::{Map, Value, json}; +use truapi::v01::{ + OperationStartedResult, RemoteChainHeadBodyRequest, RemoteChainHeadBodyResponse, + RemoteChainHeadCallRequest, RemoteChainHeadCallResponse, RemoteChainHeadContinueRequest, + RemoteChainHeadFollowItem, RemoteChainHeadFollowRequest, RemoteChainHeadHeaderRequest, + RemoteChainHeadHeaderResponse, RemoteChainHeadStopOperationRequest, + RemoteChainHeadStorageRequest, RemoteChainHeadStorageResponse, RemoteChainHeadUnpinRequest, + RemoteChainSpecChainNameResponse, RemoteChainSpecGenesisHashResponse, + RemoteChainSpecPropertiesResponse, RemoteChainTransactionBroadcastRequest, + RemoteChainTransactionBroadcastResponse, RemoteChainTransactionStopRequest, RuntimeApi, + RuntimeSpec, RuntimeType, StorageQueryItem, StorageQueryType, StorageResultItem, +}; +use truapi_platform::{GenesisHash, JsonRpcConnection}; + +use crate::subscription::Spawner; + +const FOLLOW_METHOD: &str = "remote_chain_head_follow"; + +/// Classification of framework-level chain failures separate from JSON-RPC +/// domain errors. Maps cleanly to [`truapi::CallError`] variants at the +/// `PlatformRuntimeHost` boundary. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RuntimeFailureKind { + /// Backend is not wired or refused the request for plumbing reasons. + Unavailable, + /// Backend responded but the payload was malformed or the call failed. + HostFailure, +} + +/// Framework-level chain failure with a diagnostic reason. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RuntimeFailure { + kind: RuntimeFailureKind, + method: &'static str, + reason: Option, +} + +impl RuntimeFailure { + /// Backend refused the call for unavailability reasons (no provider, the + /// connection died, etc.). + pub fn unavailable(method: &'static str) -> Self { + Self { + kind: RuntimeFailureKind::Unavailable, + method, + reason: None, + } + } + + /// Backend produced a structural error (malformed json-rpc, unexpected + /// shape, ...). + pub fn host_failure(method: &'static str, reason: impl Into) -> Self { + Self { + kind: RuntimeFailureKind::HostFailure, + method, + reason: Some(reason.into()), + } + } + + /// Failure classification. + pub fn kind(&self) -> RuntimeFailureKind { + self.kind + } + + /// Method tag the failure originated from. + pub fn method(&self) -> &'static str { + self.method + } + + /// Diagnostic reason. Always non-empty for `HostFailure`. + pub fn reason(&self) -> String { + match &self.reason { + Some(reason) => format!("{}: {}", self.method, reason), + None => self.method.to_string(), + } + } +} + +/// Provider of `JsonRpcConnection` instances keyed by chain genesis hash. +/// The default [`UnavailableChainProvider`] makes every call fail; real +/// hosts plug in either the platform-side `ChainProvider` or the bundled +/// smoldot provider (feature `smoldot`). +#[async_trait::async_trait] +pub trait RuntimeChainProvider: Send + Sync { + /// Open or reuse a JSON-RPC connection for the chain identified by + /// `genesis_hash`. + async fn connect( + &self, + genesis_hash: GenesisHash, + ) -> Result, RuntimeFailure>; +} + +/// Default provider: every `connect` call fails with `Unavailable`, so each +/// chain RPC surfaces a typed "unavailable" error to the product. +#[derive(Default)] +pub struct UnavailableChainProvider; + +#[async_trait::async_trait] +impl RuntimeChainProvider for UnavailableChainProvider { + async fn connect( + &self, + _genesis_hash: GenesisHash, + ) -> Result, RuntimeFailure> { + Err(RuntimeFailure::unavailable("remote_chain_connect")) + } +} + +/// chainHead-v1 state machine on top of a [`RuntimeChainProvider`]. +/// +/// Each method maps a typed v01 chain request to one or more json-rpc calls, +/// shares one `chainHead_v1_follow` subscription per (genesis_hash, local +/// follow id) pair, and parses follow events back into typed +/// [`RemoteChainHeadFollowItem`] values. +#[derive(Clone)] +pub struct ChainRuntime { + provider: Arc, + spawner: Spawner, + connections: Arc>>>, +} + +impl ChainRuntime { + /// Build a `ChainRuntime` driven by `provider`. Background tasks (response + /// pumps, follow setup) are spawned on `spawner`. + pub fn new(provider: Arc, spawner: Spawner) -> Self { + Self { + provider, + spawner, + connections: Arc::new(Mutex::new(HashMap::new())), + } + } + + /// Start (or attach to an existing) `chainHead_v1_follow` subscription. + /// Returns a stream of typed follow items that closes when the remote + /// sends `stop` or the connection drops. + pub fn remote_chain_head_follow( + &self, + follow_subscription_id: String, + request: RemoteChainHeadFollowRequest, + ) -> BoxStream<'static, RemoteChainHeadFollowItem> { + let (tx, rx) = mpsc::unbounded(); + let runtime = self.clone(); + let cleanup_runtime = self.clone(); + let cleanup_genesis_hash = request.genesis_hash.clone(); + let cleanup_follow_id = follow_subscription_id.clone(); + + let fut = async move { + if runtime + .start_follow(follow_subscription_id, request, Some(tx.clone())) + .await + .is_err() + { + let _ = tx.unbounded_send(FollowSignal::Interrupt); + } + }; + (self.spawner)(fut.boxed()); + + let stream = ManagedSubscription::new( + rx.boxed(), + Some(Box::new(move || { + cleanup_runtime.cleanup_follow(&cleanup_genesis_hash, &cleanup_follow_id); + })), + ); + stream + .filter_map(|signal| async move { + match signal { + FollowSignal::Item(item) => Some(item), + FollowSignal::Interrupt => None, + } + }) + .boxed() + } + + /// Fetch a block header. + pub async fn remote_chain_head_header( + &self, + request: RemoteChainHeadHeaderRequest, + ) -> Result { + let method = "remote_chain_head_header"; + let connection = self.connection_for(method, &request.genesis_hash).await?; + let remote_follow_id = self + .ensure_follow_context(method, &connection, request.follow_subscription_id, false) + .await?; + + let value = connection + .request_value( + method, + "chainHead_v1_header", + json!([remote_follow_id, encode_hex(&request.hash)]), + ) + .await?; + let header = match value { + Value::Null => None, + Value::String(encoded) => Some( + decode_hex(&encoded) + .map_err(|reason| RuntimeFailure::host_failure(method, reason))?, + ), + _ => { + return Err(RuntimeFailure::host_failure( + method, + "unexpected chainHead_v1_header result", + )); + } + }; + Ok(RemoteChainHeadHeaderResponse { header }) + } + + /// Start a chainHead_v1_body operation. + pub async fn remote_chain_head_body( + &self, + request: RemoteChainHeadBodyRequest, + ) -> Result { + let method = "remote_chain_head_body"; + let connection = self.connection_for(method, &request.genesis_hash).await?; + let remote_follow_id = self + .ensure_follow_context(method, &connection, request.follow_subscription_id, false) + .await?; + + let value = connection + .request_value( + method, + "chainHead_v1_body", + json!([remote_follow_id, encode_hex(&request.hash)]), + ) + .await?; + let operation = operation_started_result(method, value)?; + Ok(RemoteChainHeadBodyResponse { operation }) + } + + /// Start a chainHead_v1_storage operation. + pub async fn remote_chain_head_storage( + &self, + request: RemoteChainHeadStorageRequest, + ) -> Result { + let method = "remote_chain_head_storage"; + let connection = self.connection_for(method, &request.genesis_hash).await?; + let remote_follow_id = self + .ensure_follow_context(method, &connection, request.follow_subscription_id, false) + .await?; + + let items = request + .items + .iter() + .map(encode_storage_query_item) + .collect::>(); + let child_trie = request + .child_trie + .as_ref() + .map(|bytes| Value::String(encode_hex(bytes))) + .unwrap_or(Value::Null); + + let value = connection + .request_value( + method, + "chainHead_v1_storage", + json!([ + remote_follow_id, + encode_hex(&request.hash), + Value::Array(items), + child_trie, + ]), + ) + .await?; + let operation = operation_started_result(method, value)?; + Ok(RemoteChainHeadStorageResponse { operation }) + } + + /// Start a chainHead_v1_call operation. + pub async fn remote_chain_head_call( + &self, + request: RemoteChainHeadCallRequest, + ) -> Result { + let method = "remote_chain_head_call"; + let connection = self.connection_for(method, &request.genesis_hash).await?; + let remote_follow_id = self + .ensure_follow_context(method, &connection, request.follow_subscription_id, true) + .await?; + + let value = connection + .request_value( + method, + "chainHead_v1_call", + json!([ + remote_follow_id, + encode_hex(&request.hash), + request.function, + encode_hex(&request.call_parameters), + ]), + ) + .await?; + let operation = operation_started_result(method, value)?; + Ok(RemoteChainHeadCallResponse { operation }) + } + + /// Release pinned blocks. + pub async fn remote_chain_head_unpin( + &self, + request: RemoteChainHeadUnpinRequest, + ) -> Result<(), RuntimeFailure> { + let method = "remote_chain_head_unpin"; + let connection = self.connection_for(method, &request.genesis_hash).await?; + let remote_follow_id = self + .ensure_follow_context(method, &connection, request.follow_subscription_id, false) + .await?; + let hashes: Vec = request + .hashes + .iter() + .map(|hash| Value::String(encode_hex(hash))) + .collect(); + let value = connection + .request_value( + method, + "chainHead_v1_unpin", + json!([remote_follow_id, Value::Array(hashes)]), + ) + .await?; + match value { + Value::Null => Ok(()), + _ => Err(RuntimeFailure::host_failure( + method, + "unexpected chainHead_v1_unpin result", + )), + } + } + + /// Continue a paused operation. + pub async fn remote_chain_head_continue( + &self, + request: RemoteChainHeadContinueRequest, + ) -> Result<(), RuntimeFailure> { + let method = "remote_chain_head_continue"; + let connection = self.connection_for(method, &request.genesis_hash).await?; + let remote_follow_id = self + .ensure_follow_context(method, &connection, request.follow_subscription_id, false) + .await?; + let value = connection + .request_value( + method, + "chainHead_v1_continue", + json!([remote_follow_id, request.operation_id]), + ) + .await?; + match value { + Value::Null => Ok(()), + _ => Err(RuntimeFailure::host_failure( + method, + "unexpected chainHead_v1_continue result", + )), + } + } + + /// Stop a chain-head operation. + pub async fn remote_chain_head_stop_operation( + &self, + request: RemoteChainHeadStopOperationRequest, + ) -> Result<(), RuntimeFailure> { + let method = "remote_chain_head_stop_operation"; + let connection = self.connection_for(method, &request.genesis_hash).await?; + let remote_follow_id = self + .ensure_follow_context(method, &connection, request.follow_subscription_id, false) + .await?; + let value = connection + .request_value( + method, + "chainHead_v1_stopOperation", + json!([remote_follow_id, request.operation_id]), + ) + .await?; + match value { + Value::Null => Ok(()), + _ => Err(RuntimeFailure::host_failure( + method, + "unexpected chainHead_v1_stopOperation result", + )), + } + } + + /// Echo back the chain genesis hash via chainSpec_v1_genesisHash. + pub async fn remote_chain_spec_genesis_hash( + &self, + genesis_hash: GenesisHash, + ) -> Result { + let method = "remote_chain_spec_genesis_hash"; + let connection = self.connection_for(method, &genesis_hash).await?; + let value = connection + .request_value(method, "chainSpec_v1_genesisHash", json!([])) + .await?; + match value { + Value::String(encoded) => { + let bytes = decode_hex(&encoded) + .map_err(|reason| RuntimeFailure::host_failure(method, reason))?; + Ok(RemoteChainSpecGenesisHashResponse { + genesis_hash: bytes, + }) + } + _ => Err(RuntimeFailure::host_failure( + method, + "unexpected chainSpec_v1_genesisHash result", + )), + } + } + + /// Fetch the chain display name via chainSpec_v1_chainName. + pub async fn remote_chain_spec_chain_name( + &self, + genesis_hash: GenesisHash, + ) -> Result { + let method = "remote_chain_spec_chain_name"; + let connection = self.connection_for(method, &genesis_hash).await?; + let value = connection + .request_value(method, "chainSpec_v1_chainName", json!([])) + .await?; + match value { + Value::String(name) => Ok(RemoteChainSpecChainNameResponse { chain_name: name }), + _ => Err(RuntimeFailure::host_failure( + method, + "unexpected chainSpec_v1_chainName result", + )), + } + } + + /// Fetch the chain JSON properties via chainSpec_v1_properties. + pub async fn remote_chain_spec_properties( + &self, + genesis_hash: GenesisHash, + ) -> Result { + let method = "remote_chain_spec_properties"; + let connection = self.connection_for(method, &genesis_hash).await?; + let value = connection + .request_value(method, "chainSpec_v1_properties", json!([])) + .await?; + let properties = serde_json::to_string(&value) + .map_err(|err| RuntimeFailure::host_failure(method, err.to_string()))?; + Ok(RemoteChainSpecPropertiesResponse { properties }) + } + + /// Broadcast a signed transaction via transaction_v1_broadcast. + pub async fn remote_chain_transaction_broadcast( + &self, + request: RemoteChainTransactionBroadcastRequest, + ) -> Result { + let method = "remote_chain_transaction_broadcast"; + let connection = self.connection_for(method, &request.genesis_hash).await?; + let value = connection + .request_value( + method, + "transaction_v1_broadcast", + json!([encode_hex(&request.transaction)]), + ) + .await?; + match value { + Value::Null => Ok(RemoteChainTransactionBroadcastResponse { operation_id: None }), + Value::String(operation_id) => Ok(RemoteChainTransactionBroadcastResponse { + operation_id: Some(operation_id), + }), + _ => Err(RuntimeFailure::host_failure( + method, + "unexpected transaction_v1_broadcast result", + )), + } + } + + /// Stop a transaction broadcast via transaction_v1_stop. + pub async fn remote_chain_transaction_stop( + &self, + request: RemoteChainTransactionStopRequest, + ) -> Result<(), RuntimeFailure> { + let method = "remote_chain_transaction_stop"; + let connection = self.connection_for(method, &request.genesis_hash).await?; + let value = connection + .request_value(method, "transaction_v1_stop", json!([request.operation_id])) + .await?; + match value { + Value::Null => Ok(()), + _ => Err(RuntimeFailure::host_failure( + method, + "unexpected transaction_v1_stop result", + )), + } + } + + async fn connection_for( + &self, + method: &'static str, + genesis_hash: &GenesisHash, + ) -> Result, RuntimeFailure> { + let key = encode_hex(genesis_hash); + if let Some(connection) = self.connections.lock().unwrap().get(&key).cloned() { + if !connection.is_closed() { + return Ok(connection); + } + self.connections.lock().unwrap().remove(&key); + } + + let rpc = + self.provider + .connect(genesis_hash.clone()) + .await + .map_err(|failure| match failure.kind() { + RuntimeFailureKind::Unavailable => RuntimeFailure::unavailable(method), + RuntimeFailureKind::HostFailure => { + RuntimeFailure::host_failure(method, failure.reason()) + } + })?; + let connection = ChainConnection::new(genesis_hash.clone(), rpc, self.spawner.clone()); + self.connections + .lock() + .unwrap() + .insert(key, connection.clone()); + Ok(connection) + } + + async fn start_follow( + &self, + local_follow_id: String, + request: RemoteChainHeadFollowRequest, + sender: Option>, + ) -> Result<(), RuntimeFailure> { + let connection = self + .connection_for(FOLLOW_METHOD, &request.genesis_hash) + .await?; + if connection.follow_exists(&local_follow_id) { + if let Some(sender) = sender { + connection.attach_sender(&local_follow_id, sender); + } + return Ok(()); + } + + connection.insert_follow( + local_follow_id.clone(), + FollowState { + with_runtime: request.with_runtime, + remote_follow_id: None, + sender, + }, + ); + + let remote_follow_id = match connection + .request_value( + FOLLOW_METHOD, + "chainHead_v1_follow", + json!([request.with_runtime]), + ) + .await + { + Ok(Value::String(value)) => value, + Ok(_) => { + connection.remove_follow(&local_follow_id); + return Err(RuntimeFailure::host_failure( + FOLLOW_METHOD, + "unexpected chainHead_v1_follow result", + )); + } + Err(failure) => { + connection.remove_follow(&local_follow_id); + return Err(failure); + } + }; + + connection.set_remote_follow_id(&local_follow_id, remote_follow_id); + Ok(()) + } + + async fn ensure_follow_context( + &self, + method: &'static str, + connection: &Arc, + local_follow_id: String, + with_runtime: bool, + ) -> Result { + if let Some(remote_follow_id) = connection.remote_follow_id(&local_follow_id) { + if with_runtime && !connection.follow_with_runtime(&local_follow_id) { + return Err(RuntimeFailure::host_failure( + method, + "follow subscription was created without runtime metadata", + )); + } + return Ok(remote_follow_id); + } + + self.start_follow( + local_follow_id.clone(), + RemoteChainHeadFollowRequest { + genesis_hash: connection.genesis_hash.clone(), + with_runtime, + }, + None, + ) + .await?; + + connection + .remote_follow_id(&local_follow_id) + .ok_or_else(|| { + RuntimeFailure::host_failure(method, "follow subscription did not produce an id") + }) + } + + fn cleanup_follow(&self, genesis_hash: &GenesisHash, local_follow_id: &str) { + let key = encode_hex(genesis_hash); + let Some(connection) = self.connections.lock().unwrap().get(&key).cloned() else { + return; + }; + connection.unfollow(local_follow_id); + } +} + +/// One delivery on the local follow stream. `Interrupt` signals an +/// abnormal close (connection dropped, follow setup failed); it produces no +/// item but ends the stream. +enum FollowSignal { + Item(RemoteChainHeadFollowItem), + Interrupt, +} + +struct ChainConnection { + genesis_hash: GenesisHash, + rpc: Arc, + request_ids: AtomicU64, + closed: AtomicBool, + follows: Mutex>, + follows_by_remote: Mutex>, + pending_follow_events: Mutex>>, + requests: Mutex>, +} + +impl ChainConnection { + fn new( + genesis_hash: GenesisHash, + rpc: Arc, + spawner: Spawner, + ) -> Arc { + let connection = Arc::new(Self { + genesis_hash, + rpc, + request_ids: AtomicU64::new(1), + closed: AtomicBool::new(false), + follows: Mutex::new(HashMap::new()), + follows_by_remote: Mutex::new(HashMap::new()), + pending_follow_events: Mutex::new(HashMap::new()), + requests: Mutex::new(HashMap::new()), + }); + connection.clone().spawn_response_loop(spawner); + connection + } + + fn spawn_response_loop(self: Arc, spawner: Spawner) { + let rpc = self.rpc.clone(); + let fut = async move { + let mut responses = rpc.responses(); + while let Some(response) = responses.next().await { + if let Err(failure) = self.handle_response(&response) { + self.close_with_failure(failure); + return; + } + } + self.close_with_failure(RuntimeFailure::unavailable(FOLLOW_METHOD)); + }; + (spawner)(fut.boxed()); + } + + fn is_closed(&self) -> bool { + self.closed.load(Ordering::Relaxed) + } + + async fn request_value( + &self, + method: &'static str, + rpc_method: &'static str, + params: Value, + ) -> Result { + if self.is_closed() { + return Err(RuntimeFailure::unavailable(method)); + } + + let request_id = format!( + "truapi:{}", + self.request_ids.fetch_add(1, Ordering::Relaxed) + ); + let (tx, rx) = oneshot::channel(); + self.requests + .lock() + .unwrap() + .insert(request_id.clone(), PendingRequest { method, tx }); + self.rpc.send( + json!({ + "jsonrpc": "2.0", + "id": request_id, + "method": rpc_method, + "params": params, + }) + .to_string(), + ); + + match rx.await { + Ok(result) => result, + Err(_) => Err(RuntimeFailure::unavailable(method)), + } + } + + fn follow_exists(&self, local_follow_id: &str) -> bool { + self.follows.lock().unwrap().contains_key(local_follow_id) + } + + fn follow_with_runtime(&self, local_follow_id: &str) -> bool { + self.follows + .lock() + .unwrap() + .get(local_follow_id) + .map(|follow| follow.with_runtime) + .unwrap_or(false) + } + + fn remote_follow_id(&self, local_follow_id: &str) -> Option { + self.follows + .lock() + .unwrap() + .get(local_follow_id) + .and_then(|follow| follow.remote_follow_id.clone()) + } + + fn insert_follow(&self, local_follow_id: String, follow: FollowState) { + self.follows.lock().unwrap().insert(local_follow_id, follow); + } + + fn attach_sender(&self, local_follow_id: &str, sender: mpsc::UnboundedSender) { + if let Some(follow) = self.follows.lock().unwrap().get_mut(local_follow_id) { + follow.sender = Some(sender); + } + } + + fn set_remote_follow_id(&self, local_follow_id: &str, remote_follow_id: String) { + if let Some(follow) = self.follows.lock().unwrap().get_mut(local_follow_id) { + follow.remote_follow_id = Some(remote_follow_id.clone()); + self.follows_by_remote + .lock() + .unwrap() + .insert(remote_follow_id.clone(), local_follow_id.to_string()); + } + let buffered = self + .pending_follow_events + .lock() + .unwrap() + .remove(&remote_follow_id) + .unwrap_or_default(); + for event in buffered { + self.deliver_follow_event(local_follow_id, event); + } + } + + fn remove_follow(&self, local_follow_id: &str) { + if let Some(follow) = self.follows.lock().unwrap().remove(local_follow_id) + && let Some(remote_follow_id) = follow.remote_follow_id + { + self.follows_by_remote + .lock() + .unwrap() + .remove(&remote_follow_id); + } + } + + fn unfollow(&self, local_follow_id: &str) { + let remote_follow_id = self.remote_follow_id(local_follow_id); + self.remove_follow(local_follow_id); + let Some(remote_follow_id) = remote_follow_id else { + return; + }; + self.rpc.send( + json!({ + "jsonrpc": "2.0", + "id": format!("truapi:{}", self.request_ids.fetch_add(1, Ordering::Relaxed)), + "method": "chainHead_v1_unfollow", + "params": [remote_follow_id], + }) + .to_string(), + ); + } + + fn handle_response(&self, response: &str) -> Result<(), RuntimeFailure> { + let value: Value = serde_json::from_str(response).map_err(|error| { + RuntimeFailure::host_failure(FOLLOW_METHOD, format!("invalid json-rpc frame: {error}")) + })?; + + if value.get("method") == Some(&Value::String("chainHead_v1_followEvent".to_string())) { + return self.handle_follow_notification(&value); + } + + let Some(request_id) = value.get("id").and_then(json_id) else { + return Ok(()); + }; + let Some(pending) = self.requests.lock().unwrap().remove(&request_id) else { + return Ok(()); + }; + + if let Some(result) = value.get("result") { + let _ = pending.tx.send(Ok(result.clone())); + return Ok(()); + } + + if let Some(error) = value.get("error") { + let reason = error + .get("message") + .and_then(Value::as_str) + .unwrap_or("json-rpc error") + .to_string(); + let _ = pending + .tx + .send(Err(RuntimeFailure::host_failure(pending.method, reason))); + return Ok(()); + } + + let _ = pending.tx.send(Err(RuntimeFailure::host_failure( + pending.method, + "json-rpc response missing result and error", + ))); + Ok(()) + } + + fn handle_follow_notification(&self, value: &Value) -> Result<(), RuntimeFailure> { + let params = value + .get("params") + .and_then(Value::as_object) + .ok_or_else(|| { + RuntimeFailure::host_failure( + FOLLOW_METHOD, + "missing chainHead_v1_followEvent params", + ) + })?; + let remote_follow_id = params + .get("subscription") + .and_then(Value::as_str) + .ok_or_else(|| { + RuntimeFailure::host_failure( + FOLLOW_METHOD, + "missing chainHead_v1_followEvent subscription", + ) + })?; + let event = parse_follow_event(params.get("result").ok_or_else(|| { + RuntimeFailure::host_failure(FOLLOW_METHOD, "missing chainHead_v1_followEvent result") + })?)?; + let local_follow_id = self + .follows_by_remote + .lock() + .unwrap() + .get(remote_follow_id) + .cloned(); + match local_follow_id { + Some(local_follow_id) => self.deliver_follow_event(&local_follow_id, event), + None => { + self.pending_follow_events + .lock() + .unwrap() + .entry(remote_follow_id.to_string()) + .or_default() + .push(event); + } + } + Ok(()) + } + + fn deliver_follow_event(&self, local_follow_id: &str, event: RemoteChainHeadFollowItem) { + let sender = self + .follows + .lock() + .unwrap() + .get(local_follow_id) + .and_then(|follow| follow.sender.clone()); + let is_stop = matches!(event, RemoteChainHeadFollowItem::Stop); + if let Some(sender) = sender { + let _ = sender.unbounded_send(FollowSignal::Item(event)); + } + if is_stop { + self.remove_follow(local_follow_id); + } + } + + fn close_with_failure(&self, failure: RuntimeFailure) { + self.closed.store(true, Ordering::Relaxed); + + let requests = std::mem::take(&mut *self.requests.lock().unwrap()); + for (_, request) in requests { + let mapped = match failure.kind() { + RuntimeFailureKind::Unavailable => RuntimeFailure::unavailable(request.method), + RuntimeFailureKind::HostFailure => { + RuntimeFailure::host_failure(request.method, failure.reason()) + } + }; + let _ = request.tx.send(Err(mapped)); + } + + let follows = std::mem::take(&mut *self.follows.lock().unwrap()); + self.follows_by_remote.lock().unwrap().clear(); + self.pending_follow_events.lock().unwrap().clear(); + for (_, follow) in follows { + if let Some(sender) = follow.sender { + let _ = sender.unbounded_send(FollowSignal::Interrupt); + } + } + } +} + +struct PendingRequest { + method: &'static str, + tx: oneshot::Sender>, +} + +struct FollowState { + with_runtime: bool, + remote_follow_id: Option, + sender: Option>, +} + +/// Subscription wrapper that runs an `on_drop` cleanup when the stream is +/// dropped. Used by `remote_chain_head_follow` to send `chainHead_v1_unfollow` +/// when the local follow stream is dropped. +struct ManagedSubscription { + inner: BoxStream<'static, T>, + on_drop: Option>, +} + +impl ManagedSubscription { + fn new(inner: BoxStream<'static, T>, on_drop: Option>) -> Self { + Self { inner, on_drop } + } +} + +impl Drop for ManagedSubscription { + fn drop(&mut self) { + if let Some(on_drop) = self.on_drop.take() { + on_drop(); + } + } +} + +impl Stream for ManagedSubscription { + type Item = T; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.get_mut(); + this.inner.as_mut().poll_next(cx) + } +} + +fn parse_follow_event(value: &Value) -> Result { + let event = value + .get("event") + .and_then(Value::as_str) + .ok_or_else(|| RuntimeFailure::host_failure(FOLLOW_METHOD, "missing event name"))?; + match event { + "initialized" => Ok(RemoteChainHeadFollowItem::Initialized { + finalized_block_hashes: decode_hex_vec_field(value, "finalizedBlockHashes")?, + finalized_block_runtime: value + .get("finalizedBlockRuntime") + .map(parse_runtime_type) + .transpose() + .map_err(|reason| RuntimeFailure::host_failure(FOLLOW_METHOD, reason))?, + }), + "newBlock" => Ok(RemoteChainHeadFollowItem::NewBlock { + block_hash: decode_hex_field(value, "blockHash")?, + parent_block_hash: decode_hex_field(value, "parentBlockHash")?, + new_runtime: value + .get("newRuntime") + .map(parse_runtime_type) + .transpose() + .map_err(|reason| RuntimeFailure::host_failure(FOLLOW_METHOD, reason))?, + }), + "bestBlockChanged" => Ok(RemoteChainHeadFollowItem::BestBlockChanged { + best_block_hash: decode_hex_field(value, "bestBlockHash")?, + }), + "finalized" => Ok(RemoteChainHeadFollowItem::Finalized { + finalized_block_hashes: decode_hex_vec_field(value, "finalizedBlockHashes")?, + pruned_block_hashes: decode_hex_vec_field(value, "prunedBlockHashes")?, + }), + "operationBodyDone" => Ok(RemoteChainHeadFollowItem::OperationBodyDone { + operation_id: string_field(value, "operationId")?, + value: decode_hex_vec_field(value, "value")?, + }), + "operationCallDone" => Ok(RemoteChainHeadFollowItem::OperationCallDone { + operation_id: string_field(value, "operationId")?, + output: decode_hex_field(value, "output")?, + }), + "operationStorageItems" => Ok(RemoteChainHeadFollowItem::OperationStorageItems { + operation_id: string_field(value, "operationId")?, + items: parse_storage_result_items(value)?, + }), + "operationStorageDone" => Ok(RemoteChainHeadFollowItem::OperationStorageDone { + operation_id: string_field(value, "operationId")?, + }), + "operationWaitingForContinue" => { + Ok(RemoteChainHeadFollowItem::OperationWaitingForContinue { + operation_id: string_field(value, "operationId")?, + }) + } + "operationInaccessible" => Ok(RemoteChainHeadFollowItem::OperationInaccessible { + operation_id: string_field(value, "operationId")?, + }), + "operationError" => Ok(RemoteChainHeadFollowItem::OperationError { + operation_id: string_field(value, "operationId")?, + error: string_field(value, "error")?, + }), + "stop" => Ok(RemoteChainHeadFollowItem::Stop), + other => Err(RuntimeFailure::host_failure( + FOLLOW_METHOD, + format!("unsupported follow event {other}"), + )), + } +} + +fn parse_storage_result_items(value: &Value) -> Result, RuntimeFailure> { + let Some(items) = value.get("items").and_then(Value::as_array) else { + return Ok(Vec::new()); + }; + items + .iter() + .map(|item| -> Result { + let key = decode_hex_field(item, "key")?; + let value = optional_hex_field(item, "value")?; + let hash = optional_hex_field(item, "hash")?; + let closest_descendant_merkle_value = + optional_hex_field(item, "closestDescendantMerkleValue")?; + Ok(StorageResultItem { + key, + value, + hash, + closest_descendant_merkle_value, + }) + }) + .collect() +} + +fn operation_started_result( + method: &'static str, + value: Value, +) -> Result { + let result = value + .get("result") + .and_then(Value::as_str) + .ok_or_else(|| RuntimeFailure::host_failure(method, "missing operation result kind"))?; + match result { + "started" => Ok(OperationStartedResult::Started { + operation_id: value + .get("operationId") + .and_then(Value::as_str) + .ok_or_else(|| RuntimeFailure::host_failure(method, "missing operation id"))? + .to_string(), + }), + "limitReached" => Ok(OperationStartedResult::LimitReached), + other => Err(RuntimeFailure::host_failure( + method, + format!("unexpected operation result {other}"), + )), + } +} + +fn parse_runtime_type(value: &Value) -> Result { + let Some(kind) = value.get("type").and_then(Value::as_str) else { + return Ok(RuntimeType::Invalid { + error: "missing runtime type".to_string(), + }); + }; + match kind { + "valid" => { + let spec = value + .get("spec") + .and_then(Value::as_object) + .ok_or_else(|| "missing valid runtime spec".to_string())?; + let apis = spec + .get("apis") + .and_then(Value::as_object) + .map(|apis| { + let mut entries = apis + .iter() + .filter_map(|(name, version)| { + version.as_u64().map(|version| RuntimeApi { + name: name.clone(), + version: version as u32, + }) + }) + .collect::>(); + entries.sort_by(|left, right| left.name.cmp(&right.name)); + entries + }) + .unwrap_or_default(); + Ok(RuntimeType::Valid(RuntimeSpec { + spec_name: string_object_field(spec, "specName")?, + impl_name: string_object_field(spec, "implName")?, + spec_version: u32_object_field(spec, "specVersion")?, + impl_version: u32_object_field(spec, "implVersion")?, + transaction_version: spec + .get("transactionVersion") + .and_then(Value::as_u64) + .map(|value| value as u32), + apis, + })) + } + "invalid" => Ok(RuntimeType::Invalid { + error: value + .get("error") + .and_then(Value::as_str) + .unwrap_or("invalid runtime") + .to_string(), + }), + other => Err(format!("unsupported runtime type {other}")), + } +} + +fn encode_storage_query_item(item: &StorageQueryItem) -> Value { + let query_type = match item.query_type { + StorageQueryType::Value => "value", + StorageQueryType::Hash => "hash", + StorageQueryType::ClosestDescendantMerkleValue => "closestDescendantMerkleValue", + StorageQueryType::DescendantsValues => "descendantsValues", + StorageQueryType::DescendantsHashes => "descendantsHashes", + }; + let mut map = Map::new(); + map.insert("key".to_string(), Value::String(encode_hex(&item.key))); + map.insert("type".to_string(), Value::String(query_type.to_string())); + Value::Object(map) +} + +fn json_id(value: &Value) -> Option { + match value { + Value::String(text) => Some(text.clone()), + Value::Number(number) => Some(number.to_string()), + _ => None, + } +} + +fn decode_hex_field(value: &Value, field: &str) -> Result, RuntimeFailure> { + let string = string_field(value, field)?; + decode_hex(&string).map_err(|reason| RuntimeFailure::host_failure(FOLLOW_METHOD, reason)) +} + +fn optional_hex_field(value: &Value, field: &str) -> Result>, RuntimeFailure> { + match value.get(field) { + None | Some(Value::Null) => Ok(None), + Some(Value::String(text)) => decode_hex(text) + .map(Some) + .map_err(|reason| RuntimeFailure::host_failure(FOLLOW_METHOD, reason)), + Some(_) => Err(RuntimeFailure::host_failure( + FOLLOW_METHOD, + format!("invalid {field}"), + )), + } +} + +fn decode_hex_vec_field(value: &Value, field: &str) -> Result>, RuntimeFailure> { + let Some(values) = value.get(field).and_then(Value::as_array) else { + return Ok(Vec::new()); + }; + values + .iter() + .map(|value| { + value + .as_str() + .ok_or_else(|| { + RuntimeFailure::host_failure(FOLLOW_METHOD, format!("invalid {field}")) + }) + .and_then(|value| { + decode_hex(value) + .map_err(|reason| RuntimeFailure::host_failure(FOLLOW_METHOD, reason)) + }) + }) + .collect() +} + +fn string_field(value: &Value, field: &str) -> Result { + value + .get(field) + .and_then(Value::as_str) + .map(ToString::to_string) + .ok_or_else(|| RuntimeFailure::host_failure(FOLLOW_METHOD, format!("missing {field}"))) +} + +fn string_object_field(value: &Map, field: &str) -> Result { + value + .get(field) + .and_then(Value::as_str) + .map(ToString::to_string) + .ok_or_else(|| format!("missing {field}")) +} + +fn u32_object_field(value: &Map, field: &str) -> Result { + value + .get(field) + .and_then(Value::as_u64) + .map(|value| value as u32) + .ok_or_else(|| format!("missing {field}")) +} + +/// Encode a byte slice as a `0x`-prefixed lowercase hex string. +pub(crate) fn encode_hex(value: &[u8]) -> String { + let mut out = String::from("0x"); + for byte in value { + out.push_str(&format!("{byte:02x}")); + } + out +} + +fn decode_hex(value: &str) -> Result, String> { + let value = value.strip_prefix("0x").unwrap_or(value); + if !value.len().is_multiple_of(2) { + return Err("invalid hex length".to_string()); + } + + let mut out = Vec::with_capacity(value.len() / 2); + let bytes = value.as_bytes(); + let mut index = 0; + while index < bytes.len() { + let chunk = std::str::from_utf8(&bytes[index..index + 2]).map_err(|_| "invalid hex")?; + let byte = u8::from_str_radix(chunk, 16).map_err(|_| "invalid hex")?; + out.push(byte); + index += 2; + } + Ok(out) +} + +// --------------------------------------------------------------------------- +// Spawner adapter for the chain runtime. +// --------------------------------------------------------------------------- +// +// `ChainRuntime` uses a small wrapper future type so callers can construct +// the runtime with a generic `Spawner` without exposing the `BoxFuture` +// requirement in the public signature. + +/// Convenience: build a [`Spawner`] that runs each spawned future on a fresh +/// OS thread driven by [`futures::executor::block_on`]. Useful for tests and +/// embedders that have not yet wired a real runtime. Not available on wasm32 +/// since the platform has no threads. +#[cfg(not(target_arch = "wasm32"))] +pub fn thread_per_task_spawner() -> Spawner { + Arc::new(|fut: futures::future::BoxFuture<'static, ()>| { + std::thread::spawn(move || futures::executor::block_on(fut)); + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use async_trait::async_trait; + use futures::SinkExt; + use futures::channel::mpsc as fut_mpsc; + use futures::stream::BoxStream; + use std::sync::atomic::{AtomicUsize, Ordering}; + + fn spawner_for_tests() -> Spawner { + thread_per_task_spawner() + } + + /// Provider that echoes a canned response for every request it sees, + /// driven by a `respond` closure. The closure receives each json-rpc + /// request string and returns the response string the test wants the + /// server to deliver. Keeps the response loop synchronized with the + /// request stream so there is no race between `send` and the response + /// loop draining frames before pending requests have registered. + type Responder = Arc Option + Send + Sync>; + + struct ScriptedProvider { + respond: Responder, + sent: Arc>>, + sender: Arc>>>, + receiver: Arc>>>, + connect_calls: Arc, + } + + impl ScriptedProvider { + fn new(respond: F) -> Self + where + F: Fn(&str) -> Option + Send + Sync + 'static, + { + let (tx, rx) = fut_mpsc::unbounded(); + Self { + respond: Arc::new(respond), + sent: Arc::new(Mutex::new(Vec::new())), + sender: Arc::new(Mutex::new(Some(tx))), + receiver: Arc::new(Mutex::new(Some(rx))), + connect_calls: Arc::new(AtomicUsize::new(0)), + } + } + } + + struct ScriptedConnection { + respond: Responder, + sent: Arc>>, + sender: Arc>>>, + receiver: Mutex>>, + } + + impl JsonRpcConnection for ScriptedConnection { + fn send(&self, request: String) { + self.sent.lock().unwrap().push(request.clone()); + if let Some(response) = (self.respond)(&request) + && let Some(sender) = self.sender.lock().unwrap().as_ref() + { + let _ = sender.unbounded_send(response); + } + } + fn responses(&self) -> BoxStream<'static, String> { + let rx = self + .receiver + .lock() + .unwrap() + .take() + .expect("ScriptedConnection::responses called twice"); + rx.boxed() + } + } + + #[async_trait] + impl RuntimeChainProvider for ScriptedProvider { + async fn connect( + &self, + _genesis_hash: GenesisHash, + ) -> Result, RuntimeFailure> { + self.connect_calls.fetch_add(1, Ordering::SeqCst); + let receiver = self.receiver.lock().unwrap().take(); + Ok(Arc::new(ScriptedConnection { + respond: self.respond.clone(), + sent: self.sent.clone(), + sender: self.sender.clone(), + receiver: Mutex::new(receiver), + })) + } + } + + /// Connection backed by an mpsc channel: tests push frames into `tx` + /// to simulate asynchronous notifications. Used for follow-event tests. + struct ChannelConnection { + rx: Mutex>>, + sent: Arc>>, + } + + impl JsonRpcConnection for ChannelConnection { + fn send(&self, request: String) { + self.sent.lock().unwrap().push(request); + } + fn responses(&self) -> BoxStream<'static, String> { + let rx = self + .rx + .lock() + .unwrap() + .take() + .expect("responses taken twice"); + rx.boxed() + } + } + + struct ChannelProvider { + sender: Arc>>>, + receiver: Arc>>>, + sent: Arc>>, + } + + impl ChannelProvider { + fn new() -> Self { + let (tx, rx) = fut_mpsc::unbounded(); + Self { + sender: Arc::new(Mutex::new(Some(tx))), + receiver: Arc::new(Mutex::new(Some(rx))), + sent: Arc::new(Mutex::new(Vec::new())), + } + } + + fn take_sender(&self) -> fut_mpsc::UnboundedSender { + self.sender + .lock() + .unwrap() + .take() + .expect("sender taken twice") + } + } + + #[async_trait] + impl RuntimeChainProvider for ChannelProvider { + async fn connect( + &self, + _genesis_hash: GenesisHash, + ) -> Result, RuntimeFailure> { + let rx = self.receiver.lock().unwrap().take(); + Ok(Arc::new(ChannelConnection { + rx: Mutex::new(rx), + sent: self.sent.clone(), + })) + } + } + + #[test] + fn unavailable_provider_surfaces_failure() { + let provider = Arc::new(UnavailableChainProvider); + let result = futures::executor::block_on(provider.connect(vec![0u8; 32])); + let err = match result { + Ok(_) => panic!("expected failure"), + Err(err) => err, + }; + assert_eq!(err.kind(), RuntimeFailureKind::Unavailable); + assert_eq!(err.method(), "remote_chain_connect"); + } + + /// Find the json-rpc request id of the just-sent frame so the scripted + /// responder can mirror it back to the dispatcher. + fn extract_id(request: &str) -> Option { + let value: Value = serde_json::from_str(request).ok()?; + value.get("id")?.as_str().map(ToString::to_string) + } + + #[test] + fn header_request_routes_through_provider() { + let provider = Arc::new(ScriptedProvider::new(|request| { + let id = extract_id(request).unwrap(); + if request.contains("chainHead_v1_follow") { + Some(format!( + r#"{{"jsonrpc":"2.0","id":"{id}","result":"REMOTE-FOLLOW"}}"# + )) + } else if request.contains("chainHead_v1_header") { + Some(format!( + r#"{{"jsonrpc":"2.0","id":"{id}","result":"0xdeadbeef"}}"# + )) + } else { + None + } + })); + let runtime = ChainRuntime::new(provider.clone(), spawner_for_tests()); + let response = futures::executor::block_on(runtime.remote_chain_head_header( + RemoteChainHeadHeaderRequest { + genesis_hash: vec![0u8; 32], + follow_subscription_id: "local-follow".to_string(), + hash: vec![1u8; 32], + }, + )) + .expect("ok response"); + assert_eq!(response.header, Some(vec![0xde, 0xad, 0xbe, 0xef])); + assert_eq!(provider.connect_calls.load(Ordering::SeqCst), 1); + let sent = provider.sent.lock().unwrap().clone(); + assert_eq!(sent.len(), 2); + assert!(sent[0].contains("chainHead_v1_follow")); + assert!(sent[1].contains("chainHead_v1_header")); + } + + #[test] + fn unknown_genesis_chain_spec_propagates_failure() { + let provider = Arc::new(UnavailableChainProvider); + let runtime = ChainRuntime::new(provider, spawner_for_tests()); + let err = match futures::executor::block_on( + runtime.remote_chain_spec_chain_name(vec![0u8; 32]), + ) { + Ok(_) => panic!("expected failure"), + Err(err) => err, + }; + assert_eq!(err.kind(), RuntimeFailureKind::Unavailable); + assert_eq!(err.method(), "remote_chain_spec_chain_name"); + } + + #[test] + fn json_rpc_error_becomes_host_failure() { + let provider = Arc::new(ScriptedProvider::new(|request| { + let id = extract_id(request).unwrap(); + Some(format!( + r#"{{"jsonrpc":"2.0","id":"{id}","error":{{"code":-32601,"message":"method not found"}}}}"# + )) + })); + let runtime = ChainRuntime::new(provider, spawner_for_tests()); + let err = match futures::executor::block_on( + runtime.remote_chain_spec_chain_name(vec![0u8; 32]), + ) { + Ok(_) => panic!("expected failure"), + Err(err) => err, + }; + assert_eq!(err.kind(), RuntimeFailureKind::HostFailure); + assert!( + err.reason().contains("method not found"), + "unexpected reason: {}", + err.reason() + ); + } + + #[test] + fn follow_event_initialized_translates_to_v01_item() { + let provider = Arc::new(ChannelProvider::new()); + let tx = provider.take_sender(); + let runtime = ChainRuntime::new(provider.clone(), spawner_for_tests()); + + // The follow setup needs to wait for the rpc response, so we splice + // it in before starting the subscription. + let mut tx_owned = tx.clone(); + futures::executor::block_on(async move { + tx_owned + .send(r#"{"jsonrpc":"2.0","id":"truapi:1","result":"REMOTE-FOLLOW"}"#.to_string()) + .await + .unwrap(); + }); + + let mut stream = runtime.remote_chain_head_follow( + "local-follow".to_string(), + RemoteChainHeadFollowRequest { + genesis_hash: vec![0u8; 32], + with_runtime: false, + }, + ); + + // Now push a follow event keyed by remote subscription id. + let mut tx_owned = tx.clone(); + futures::executor::block_on(async move { + tx_owned.send( + r#"{"jsonrpc":"2.0","method":"chainHead_v1_followEvent","params":{"subscription":"REMOTE-FOLLOW","result":{"event":"initialized","finalizedBlockHashes":["0xaabbccdd"]}}}"# + .to_string(), + ).await.unwrap(); + tx_owned.send( + r#"{"jsonrpc":"2.0","method":"chainHead_v1_followEvent","params":{"subscription":"REMOTE-FOLLOW","result":{"event":"stop"}}}"# + .to_string(), + ).await.unwrap(); + }); + + let items: Vec<_> = futures::executor::block_on(async { + let mut out = Vec::new(); + while let Some(item) = stream.next().await { + let is_stop = matches!(item, RemoteChainHeadFollowItem::Stop); + out.push(item); + if is_stop { + break; + } + } + out + }); + + match &items[0] { + RemoteChainHeadFollowItem::Initialized { + finalized_block_hashes, + finalized_block_runtime, + } => { + assert_eq!(finalized_block_hashes, &vec![vec![0xaa, 0xbb, 0xcc, 0xdd]]); + assert!(finalized_block_runtime.is_none()); + } + other => panic!("expected Initialized, got {other:?}"), + } + assert!(matches!(items[1], RemoteChainHeadFollowItem::Stop)); + } + + #[test] + fn drop_follow_stream_sends_unfollow() { + let provider = Arc::new(ChannelProvider::new()); + let tx = provider.take_sender(); + let runtime = ChainRuntime::new(provider.clone(), spawner_for_tests()); + let sent = provider.sent.clone(); + + // Pre-load the follow setup response. + let mut tx_owned = tx.clone(); + futures::executor::block_on(async move { + tx_owned + .send(r#"{"jsonrpc":"2.0","id":"truapi:1","result":"REMOTE-FOLLOW"}"#.to_string()) + .await + .unwrap(); + }); + + let stream = runtime.remote_chain_head_follow( + "local-follow".to_string(), + RemoteChainHeadFollowRequest { + genesis_hash: vec![0u8; 32], + with_runtime: false, + }, + ); + + // Wait until the follow setup roundtrips and lands in `sent`. + for _ in 0..100 { + if !sent.lock().unwrap().is_empty() { + break; + } + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + drop(stream); + + // Give the cleanup a moment to run. + for _ in 0..100 { + if sent.lock().unwrap().len() >= 2 { + break; + } + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + let messages = sent.lock().unwrap().clone(); + assert!( + messages.iter().any(|m| m.contains("chainHead_v1_unfollow")), + "unfollow not sent; messages: {messages:?}", + ); + } + + #[test] + fn encode_hex_round_trip() { + let bytes = vec![0x00u8, 0x12, 0xab, 0xff]; + let s = encode_hex(&bytes); + assert_eq!(s, "0x0012abff"); + assert_eq!(decode_hex(&s).unwrap(), bytes); + } + + #[test] + fn parse_runtime_type_valid_sorts_apis() { + let value: Value = serde_json::from_str( + r#"{"type":"valid","spec":{"specName":"polkadot","implName":"parity-polkadot","specVersion":1000,"implVersion":1,"transactionVersion":24,"apis":{"0xbeef":2,"0xbabe":4}}}"#, + ) + .unwrap(); + let runtime_type = parse_runtime_type(&value).unwrap(); + match runtime_type { + RuntimeType::Valid(spec) => { + assert_eq!(spec.apis.len(), 2); + assert_eq!(spec.apis[0].name, "0xbabe"); + assert_eq!(spec.apis[1].name, "0xbeef"); + assert_eq!(spec.transaction_version, Some(24)); + } + other => panic!("expected Valid, got {other:?}"), + } + } +} diff --git a/rust/crates/truapi-server/src/core.rs b/rust/crates/truapi-server/src/core.rs index ca8982af..ad65c14e 100644 --- a/rust/crates/truapi-server/src/core.rs +++ b/rust/crates/truapi-server/src/core.rs @@ -57,7 +57,7 @@ impl TrUApiCore { where P: Platform + 'static, { - let runtime = Arc::new(PlatformRuntimeHost::new(platform)); + let runtime = Arc::new(PlatformRuntimeHost::new(platform, spawner.clone())); let session_state = runtime.session_state(); let mut dispatcher = Dispatcher::new(spawner); dispatcher::register(&mut dispatcher, runtime); diff --git a/rust/crates/truapi-server/src/lib.rs b/rust/crates/truapi-server/src/lib.rs index 25c1812e..a2792593 100644 --- a/rust/crates/truapi-server/src/lib.rs +++ b/rust/crates/truapi-server/src/lib.rs @@ -12,6 +12,7 @@ #![forbid(unsafe_code)] +pub mod chain_runtime; pub mod core; pub mod debug_log; pub mod dispatcher; @@ -23,6 +24,9 @@ pub mod transport; pub mod generated; +#[cfg(feature = "smoldot")] +pub mod smoldot_provider; + #[cfg(all(not(target_arch = "wasm32"), feature = "ws-bridge"))] pub mod ws_bridge; diff --git a/rust/crates/truapi-server/src/runtime.rs b/rust/crates/truapi-server/src/runtime.rs index 5fc3e569..e6ff760b 100644 --- a/rust/crates/truapi-server/src/runtime.rs +++ b/rust/crates/truapi-server/src/runtime.rs @@ -8,11 +8,16 @@ use std::sync::Arc; +use crate::chain_runtime::{ + ChainRuntime, RuntimeChainProvider, RuntimeFailure, RuntimeFailureKind, +}; use crate::host_logic::dotns::{NavigateDecision, parse_navigate}; use crate::host_logic::features::feature_supported; use crate::host_logic::permissions::{Decision, PermissionsService}; use crate::host_logic::session::SessionState; +use crate::subscription::Spawner; +use futures::StreamExt; use truapi::api::{ Account, Chain, Chat, Entropy, JsonRpc, LocalStorage, Payment, Permissions, Preimage, ResourceAllocation, Signing, StatementStore, System, Theme, @@ -74,16 +79,19 @@ use truapi::versioned::system::{ }; use truapi::{CallContext, CallError, Subscription}; use truapi_platform::{ - Accounts as PlatformAccounts, Navigation as PlatformNavigation, - Notifications as PlatformNotifications, Platform, Preimage as PlatformPreimage, - Signing as PlatformSigning, StatementStore as PlatformStatementStore, - Storage as PlatformStorage, + Accounts as PlatformAccounts, ChainProvider as PlatformChainProvider, GenesisHash, + JsonRpcConnection, Navigation as PlatformNavigation, Notifications as PlatformNotifications, + Platform, Preimage as PlatformPreimage, Signing as PlatformSigning, + StatementStore as PlatformStatementStore, Storage as PlatformStorage, }; /// Adapter that exposes a [`truapi_platform::Platform`] through the /// `truapi::api::*` trait set the generated dispatcher routes to. pub struct PlatformRuntimeHost

{ platform: Arc

, + /// chainHead-v1 state machine. The provider adapter forwards + /// [`PlatformChainProvider::connect`] into the json-rpc layer. + chain: ChainRuntime, /// Currently-paired session, pushed by the host through a side channel. /// Account-management subscriptions read from this in lieu of round-tripping /// a callback on every connection-status query. @@ -92,12 +100,19 @@ pub struct PlatformRuntimeHost

{ impl

PlatformRuntimeHost

{ /// Wrap a platform implementation. The runtime takes ownership via `Arc`. - pub fn new(platform: Arc

) -> Self + /// `spawner` is used by the embedded chain runtime to drive json-rpc + /// response loops and follow-setup futures. + pub fn new(platform: Arc

, spawner: Spawner) -> Self where P: Platform + 'static, { + let chain_provider: Arc = + Arc::new(PlatformChainRuntimeProvider { + platform: platform.clone(), + }); Self { platform, + chain: ChainRuntime::new(chain_provider, spawner), session_state: SessionState::new(), } } @@ -109,12 +124,47 @@ impl

PlatformRuntimeHost

{ } } +/// Adapter from `truapi_platform::ChainProvider` into the +/// [`RuntimeChainProvider`] surface the chain runtime expects. +/// Reuses the platform-supplied json-rpc connection and converts the +/// platform `GenericError` into a `RuntimeFailure::Unavailable`. +struct PlatformChainRuntimeProvider

{ + platform: Arc

, +} + +#[async_trait::async_trait] +impl

RuntimeChainProvider for PlatformChainRuntimeProvider

+where + P: Platform + 'static, +{ + async fn connect( + &self, + genesis_hash: GenesisHash, + ) -> Result, RuntimeFailure> { + PlatformChainProvider::connect(self.platform.as_ref(), genesis_hash) + .await + .map(Arc::from) + .map_err(|_| RuntimeFailure::unavailable("remote_chain_connect")) + } +} + fn unsupported_with_reason(reason: &str) -> CallError { CallError::HostFailure { reason: reason.to_string(), } } +fn runtime_failure_to_call_error(failure: RuntimeFailure) -> CallError { + match failure.kind() { + RuntimeFailureKind::Unavailable => CallError::HostFailure { + reason: failure.reason(), + }, + RuntimeFailureKind::HostFailure => CallError::HostFailure { + reason: failure.reason(), + }, + } +} + // --------------------------------------------------------------------------- // System // --------------------------------------------------------------------------- @@ -448,9 +498,12 @@ where // Chain // --------------------------------------------------------------------------- // -// Every method on the Chain trait is stubbed as `CallError::Unsupported`. -// The platform exposes only the raw `ChainProvider::connect` JSON-RPC bridge; -// the typed chainHead surface is not implemented here. +// The chain surface is backed by `ChainRuntime`, which keeps one +// `chainHead_v1` connection per genesis hash on top of the platform-supplied +// `ChainProvider::connect`. Requests go through `request_value` and parse +// json-rpc responses into typed v01 results; follow notifications are +// translated into `RemoteChainHeadFollowItem` items on the subscription +// stream. impl

Chain for PlatformRuntimeHost

where @@ -458,112 +511,178 @@ where { async fn follow_head_subscribe( &self, - _cx: &CallContext, - _request: RemoteChainHeadFollowRequest, + cx: &CallContext, + request: RemoteChainHeadFollowRequest, ) -> Subscription { - Subscription::empty() + let RemoteChainHeadFollowRequest::V1(inner) = request; + let follow_subscription_id = cx.request_id().to_string(); + let stream = self + .chain + .remote_chain_head_follow(follow_subscription_id, inner) + .map(RemoteChainHeadFollowItem::V1); + Subscription::new(Box::pin(stream)) } async fn get_head_header( &self, _cx: &CallContext, - _request: RemoteChainHeadHeaderRequest, + request: RemoteChainHeadHeaderRequest, ) -> Result> { - Err(CallError::Unsupported) + let RemoteChainHeadHeaderRequest::V1(inner) = request; + self.chain + .remote_chain_head_header(inner) + .await + .map(RemoteChainHeadHeaderResponse::V1) + .map_err(runtime_failure_to_call_error) } async fn get_head_body( &self, _cx: &CallContext, - _request: RemoteChainHeadBodyRequest, + request: RemoteChainHeadBodyRequest, ) -> Result> { - Err(CallError::Unsupported) + let RemoteChainHeadBodyRequest::V1(inner) = request; + self.chain + .remote_chain_head_body(inner) + .await + .map(RemoteChainHeadBodyResponse::V1) + .map_err(runtime_failure_to_call_error) } async fn get_head_storage( &self, _cx: &CallContext, - _request: RemoteChainHeadStorageRequest, + request: RemoteChainHeadStorageRequest, ) -> Result> { - Err(CallError::Unsupported) + let RemoteChainHeadStorageRequest::V1(inner) = request; + self.chain + .remote_chain_head_storage(inner) + .await + .map(RemoteChainHeadStorageResponse::V1) + .map_err(runtime_failure_to_call_error) } async fn call_head( &self, _cx: &CallContext, - _request: RemoteChainHeadCallRequest, + request: RemoteChainHeadCallRequest, ) -> Result> { - Err(CallError::Unsupported) + let RemoteChainHeadCallRequest::V1(inner) = request; + self.chain + .remote_chain_head_call(inner) + .await + .map(RemoteChainHeadCallResponse::V1) + .map_err(runtime_failure_to_call_error) } async fn unpin_head( &self, _cx: &CallContext, - _request: RemoteChainHeadUnpinRequest, + request: RemoteChainHeadUnpinRequest, ) -> Result> { - Err(CallError::Unsupported) + let RemoteChainHeadUnpinRequest::V1(inner) = request; + self.chain + .remote_chain_head_unpin(inner) + .await + .map(|()| RemoteChainHeadUnpinResponse::V1) + .map_err(runtime_failure_to_call_error) } async fn continue_head( &self, _cx: &CallContext, - _request: RemoteChainHeadContinueRequest, + request: RemoteChainHeadContinueRequest, ) -> Result> { - Err(CallError::Unsupported) + let RemoteChainHeadContinueRequest::V1(inner) = request; + self.chain + .remote_chain_head_continue(inner) + .await + .map(|()| RemoteChainHeadContinueResponse::V1) + .map_err(runtime_failure_to_call_error) } async fn stop_head_operation( &self, _cx: &CallContext, - _request: RemoteChainHeadStopOperationRequest, + request: RemoteChainHeadStopOperationRequest, ) -> Result> { - Err(CallError::Unsupported) + let RemoteChainHeadStopOperationRequest::V1(inner) = request; + self.chain + .remote_chain_head_stop_operation(inner) + .await + .map(|()| RemoteChainHeadStopOperationResponse::V1) + .map_err(runtime_failure_to_call_error) } async fn get_spec_genesis_hash( &self, _cx: &CallContext, - _request: RemoteChainSpecGenesisHashRequest, + request: RemoteChainSpecGenesisHashRequest, ) -> Result> { - Err(CallError::Unsupported) + let RemoteChainSpecGenesisHashRequest::V1(inner) = request; + self.chain + .remote_chain_spec_genesis_hash(inner.genesis_hash) + .await + .map(RemoteChainSpecGenesisHashResponse::V1) + .map_err(runtime_failure_to_call_error) } async fn get_spec_chain_name( &self, _cx: &CallContext, - _request: RemoteChainSpecChainNameRequest, + request: RemoteChainSpecChainNameRequest, ) -> Result> { - Err(CallError::Unsupported) + let RemoteChainSpecChainNameRequest::V1(inner) = request; + self.chain + .remote_chain_spec_chain_name(inner.genesis_hash) + .await + .map(RemoteChainSpecChainNameResponse::V1) + .map_err(runtime_failure_to_call_error) } async fn get_spec_properties( &self, _cx: &CallContext, - _request: RemoteChainSpecPropertiesRequest, + request: RemoteChainSpecPropertiesRequest, ) -> Result> { - Err(CallError::Unsupported) + let RemoteChainSpecPropertiesRequest::V1(inner) = request; + self.chain + .remote_chain_spec_properties(inner.genesis_hash) + .await + .map(RemoteChainSpecPropertiesResponse::V1) + .map_err(runtime_failure_to_call_error) } async fn broadcast_transaction( &self, _cx: &CallContext, - _request: RemoteChainTransactionBroadcastRequest, + request: RemoteChainTransactionBroadcastRequest, ) -> Result< RemoteChainTransactionBroadcastResponse, CallError, > { - Err(CallError::Unsupported) + let RemoteChainTransactionBroadcastRequest::V1(inner) = request; + self.chain + .remote_chain_transaction_broadcast(inner) + .await + .map(RemoteChainTransactionBroadcastResponse::V1) + .map_err(runtime_failure_to_call_error) } async fn stop_transaction( &self, _cx: &CallContext, - _request: RemoteChainTransactionStopRequest, + request: RemoteChainTransactionStopRequest, ) -> Result> { - Err(CallError::Unsupported) + let RemoteChainTransactionStopRequest::V1(inner) = request; + self.chain + .remote_chain_transaction_stop(inner) + .await + .map(|()| RemoteChainTransactionStopResponse::V1) + .map_err(runtime_failure_to_call_error) } } @@ -586,6 +705,7 @@ impl

Theme for PlatformRuntimeHost

where P: Platform + 'static {} #[cfg(test)] mod tests { use super::*; + use crate::chain_runtime::thread_per_task_spawner; use async_trait::async_trait; use futures::stream::{self, BoxStream}; use parity_scale_codec::Encode; @@ -598,6 +718,10 @@ mod tests { StatementStore as PlatformStatementStore, Storage as PlatformStorage, }; + fn test_spawner() -> Spawner { + thread_per_task_spawner() + } + /// Minimal Platform impl that only answers `feature_supported`. Every /// other callback returns a unit value or empty stream, so the runtime /// can exercise its delegation paths without pulling in a real backend. @@ -793,7 +917,7 @@ mod tests { #[test] fn feature_supported_round_trips_through_runtime() { - let host = PlatformRuntimeHost::new(Arc::new(StubPlatform)); + let host = PlatformRuntimeHost::new(Arc::new(StubPlatform), test_spawner()); let cx = CallContext::new(); let request = HostFeatureSupportedRequest::V1(v01::HostFeatureSupportedRequest::Chain { genesis_hash: vec![0u8; 32], @@ -805,7 +929,7 @@ mod tests { #[test] fn navigate_to_uses_dotns_decision_and_then_platform() { - let host = PlatformRuntimeHost::new(Arc::new(StubPlatform)); + let host = PlatformRuntimeHost::new(Arc::new(StubPlatform), test_spawner()); let cx = CallContext::new(); let request = HostNavigateToRequest::V1(v01::HostNavigateToRequest { url: "mytestapp.dot".to_string(), @@ -816,7 +940,7 @@ mod tests { #[test] fn navigate_to_rejects_empty_input_without_calling_platform() { - let host = PlatformRuntimeHost::new(Arc::new(StubPlatform)); + let host = PlatformRuntimeHost::new(Arc::new(StubPlatform), test_spawner()); let cx = CallContext::new(); let request = HostNavigateToRequest::V1(v01::HostNavigateToRequest { url: "".to_string(), @@ -832,7 +956,7 @@ mod tests { #[test] fn request_login_returns_unsupported() { - let host = PlatformRuntimeHost::new(Arc::new(StubPlatform)); + let host = PlatformRuntimeHost::new(Arc::new(StubPlatform), test_spawner()); let cx = CallContext::new(); let request = HostRequestLoginRequest::V1(v01::HostRequestLoginRequest { reason: None }); let err = futures::executor::block_on(host.request_login(&cx, request)).unwrap_err(); @@ -841,7 +965,7 @@ mod tests { #[test] fn permissions_grants_and_caches() { - let host = PlatformRuntimeHost::new(Arc::new(StubPlatform)); + let host = PlatformRuntimeHost::new(Arc::new(StubPlatform), test_spawner()); let cx = CallContext::new(); let request = HostDevicePermissionRequest::V1(v01::HostDevicePermissionRequest::Camera); let response = @@ -852,7 +976,7 @@ mod tests { #[test] fn feature_supported_encodes_response_to_known_bytes() { - let host = PlatformRuntimeHost::new(Arc::new(StubPlatform)); + let host = PlatformRuntimeHost::new(Arc::new(StubPlatform), test_spawner()); let cx = CallContext::new(); let request = HostFeatureSupportedRequest::V1(v01::HostFeatureSupportedRequest::Chain { genesis_hash: vec![0u8; 32], diff --git a/rust/crates/truapi-server/src/smoldot_provider/mod.rs b/rust/crates/truapi-server/src/smoldot_provider/mod.rs new file mode 100644 index 00000000..7fcbf279 --- /dev/null +++ b/rust/crates/truapi-server/src/smoldot_provider/mod.rs @@ -0,0 +1,309 @@ +//! Rust-owned light client backend for [`RuntimeChainProvider`]. +//! +//! Wraps a single `smoldot_light::Client` that owns Paseo + Asset Hub Paseo. +//! Each `connect(genesis_hash)` returns a [`JsonRpcConnection`] that forwards +//! requests to the corresponding smoldot chain and streams responses back to +//! the chain runtime. + +use std::collections::HashMap; +use std::fmt; +use std::sync::{Arc, Mutex}; + +use futures::SinkExt; +use futures::StreamExt; +use futures::channel::mpsc; +use futures::stream::BoxStream; +use smoldot_light::{ + AddChainConfig, AddChainConfigJsonRpc, AddChainError, ChainId, Client, JsonRpcResponses, +}; +use truapi_platform::{GenesisHash, JsonRpcConnection}; + +use crate::chain_runtime::{RuntimeChainProvider, RuntimeFailure}; + +#[cfg(not(target_arch = "wasm32"))] +mod native_platform; +#[cfg(not(target_arch = "wasm32"))] +use native_platform::{PlatformRefAlias, make_platform}; + +#[cfg(target_arch = "wasm32")] +mod wasm_helpers; +#[cfg(target_arch = "wasm32")] +mod wasm_platform; +#[cfg(target_arch = "wasm32")] +mod wasm_socket; +#[cfg(target_arch = "wasm32")] +use wasm_platform::{PlatformRefAlias, make_platform}; + +const PASEO_SPEC: &str = include_str!("specs/paseo.json"); +const ASSET_HUB_PASEO_SPEC: &str = include_str!("specs/asset-hub-paseo.json"); + +const PASEO_RELAY_GENESIS: &str = + "0x77afd6190f1554ad45fd0d31aee62aacc33c6db0ea801129acb813f913e0764f"; +const ASSET_HUB_PASEO_GENESIS: &str = + "0xd6eec26135305a8ad257a20d003357284c8aa03d0bdb2b357ab0a22371e11ef2"; + +/// Errors returned by [`SmoldotChainProvider::with_bundled_specs`]. +#[derive(Debug)] +pub enum SmoldotInitError { + /// Failed to add the Paseo relay chain to the client. + AddRelay(AddChainError), + /// Failed to add the Asset Hub parachain to the client. + AddParaChain(AddChainError), +} + +impl fmt::Display for SmoldotInitError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::AddRelay(err) => write!(f, "failed to add relay chain: {err}"), + Self::AddParaChain(err) => write!(f, "failed to add parachain: {err}"), + } + } +} + +impl std::error::Error for SmoldotInitError {} + +struct ChainEntry { + chain_id: ChainId, + responses: Mutex>>, +} + +type ClientRef = Arc>>; + +/// A [`RuntimeChainProvider`] backed by `smoldot_light::Client`. +/// +/// Built via [`SmoldotChainProvider::with_bundled_specs`] with Paseo + Asset +/// Hub Paseo pre-loaded. +pub struct SmoldotChainProvider { + client: ClientRef, + chains: HashMap>, +} + +impl SmoldotChainProvider { + /// Build a provider with Paseo + Asset Hub Paseo specs already added to + /// the client. Each chain's json-rpc bus is held until the first + /// `connect` call drains it. + pub fn with_bundled_specs() -> Result { + let platform = make_platform(); + let mut client: Client = Client::new(platform); + + let relay = client + .add_chain(AddChainConfig { + specification: PASEO_SPEC, + json_rpc: AddChainConfigJsonRpc::Enabled { + max_pending_requests: std::num::NonZeroU32::new(u32::MAX).unwrap(), + max_subscriptions: u32::MAX, + }, + database_content: "", + potential_relay_chains: std::iter::empty(), + user_data: (), + statement_protocol_config: None, + }) + .map_err(SmoldotInitError::AddRelay)?; + + let para = client + .add_chain(AddChainConfig { + specification: ASSET_HUB_PASEO_SPEC, + json_rpc: AddChainConfigJsonRpc::Enabled { + max_pending_requests: std::num::NonZeroU32::new(u32::MAX).unwrap(), + max_subscriptions: u32::MAX, + }, + database_content: "", + potential_relay_chains: std::iter::once(relay.chain_id), + user_data: (), + statement_protocol_config: None, + }) + .map_err(SmoldotInitError::AddParaChain)?; + + let mut chains = HashMap::new(); + chains.insert( + PASEO_RELAY_GENESIS.to_string(), + Arc::new(ChainEntry { + chain_id: relay.chain_id, + responses: Mutex::new(relay.json_rpc_responses), + }), + ); + chains.insert( + ASSET_HUB_PASEO_GENESIS.to_string(), + Arc::new(ChainEntry { + chain_id: para.chain_id, + responses: Mutex::new(para.json_rpc_responses), + }), + ); + + Ok(Self { + client: Arc::new(Mutex::new(client)), + chains, + }) + } +} + +fn encode_genesis_hash(hash: &GenesisHash) -> String { + let mut out = String::with_capacity(2 + hash.len() * 2); + out.push_str("0x"); + for byte in hash { + out.push_str(&format!("{byte:02x}")); + } + out +} + +#[async_trait::async_trait] +impl RuntimeChainProvider for SmoldotChainProvider { + async fn connect( + &self, + genesis_hash: GenesisHash, + ) -> Result, RuntimeFailure> { + let key = encode_genesis_hash(&genesis_hash); + let entry = self + .chains + .get(&key) + .cloned() + .ok_or_else(|| RuntimeFailure::unavailable("remote_chain_connect"))?; + + let responses = entry + .responses + .lock() + .unwrap() + .take() + .ok_or_else(|| RuntimeFailure::unavailable("remote_chain_connect"))?; + + Ok(Arc::new(SmoldotJsonRpcConnection::new( + self.client.clone(), + entry.chain_id, + responses, + ))) + } +} + +struct SmoldotJsonRpcConnection { + client: ClientRef, + chain_id: ChainId, + responses: Mutex>>, +} + +impl SmoldotJsonRpcConnection { + fn new( + client: ClientRef, + chain_id: ChainId, + mut responses: JsonRpcResponses, + ) -> Self { + let (mut tx, rx) = mpsc::unbounded::(); + spawn_pump(async move { + while let Some(response) = responses.next().await { + if tx.send(response).await.is_err() { + break; + } + } + }); + Self { + client, + chain_id, + responses: Mutex::new(Some(rx)), + } + } +} + +impl JsonRpcConnection for SmoldotJsonRpcConnection { + fn send(&self, request: String) { + let mut client = self.client.lock().unwrap(); + if let Err(err) = client.json_rpc_request(request, self.chain_id) { + log_send_error(err); + } + } + + fn responses(&self) -> BoxStream<'static, String> { + let rx = self + .responses + .lock() + .unwrap() + .take() + .expect("SmoldotJsonRpcConnection::responses() called twice"); + rx.boxed() + } +} + +#[cfg(not(target_arch = "wasm32"))] +fn spawn_pump(future: F) +where + F: std::future::Future + Send + 'static, +{ + std::thread::spawn(move || { + futures::executor::block_on(future); + }); +} + +#[cfg(target_arch = "wasm32")] +fn spawn_pump(future: F) +where + F: std::future::Future + 'static, +{ + wasm_bindgen_futures::spawn_local(future); +} + +#[cfg(not(target_arch = "wasm32"))] +fn log_send_error(err: smoldot_light::HandleRpcError) { + eprintln!("smoldot json_rpc_request failed: {err}"); +} + +#[cfg(target_arch = "wasm32")] +fn log_send_error(err: smoldot_light::HandleRpcError) { + web_sys::console::error_1(&format!("smoldot json_rpc_request failed: {err}").into()); +} + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use super::*; + use std::time::{Duration, Instant}; + + fn paseo_genesis_bytes() -> GenesisHash { + let clean = PASEO_RELAY_GENESIS.trim_start_matches("0x"); + let mut out = Vec::with_capacity(32); + for pair in clean.as_bytes().chunks(2) { + out.push(u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap()); + } + out + } + + /// Smoke test: building the provider with bundled specs must register + /// the two known chains. Verifies the smoldot module compiles and the + /// client accepts the bundled chainspecs without doing any network I/O. + #[test] + fn smoldot_module_compiles_and_starts_idle_runtime() { + let provider = SmoldotChainProvider::with_bundled_specs().expect("init"); + assert!(provider.chains.contains_key(PASEO_RELAY_GENESIS)); + assert!(provider.chains.contains_key(ASSET_HUB_PASEO_GENESIS)); + drop(provider); + } + + #[test] + fn connect_unknown_genesis_is_unavailable() { + let provider = SmoldotChainProvider::with_bundled_specs().expect("init"); + let result = futures::executor::block_on(provider.connect(vec![0u8; 32])); + assert!(result.is_err()); + } + + #[test] + #[ignore = "hits the live Paseo relay network; run manually with --ignored"] + fn streams_initialized_event_for_paseo() { + let provider = SmoldotChainProvider::with_bundled_specs().expect("init"); + let connection = + futures::executor::block_on(provider.connect(paseo_genesis_bytes())).expect("connect"); + connection.send( + r#"{"jsonrpc":"2.0","id":"1","method":"chainHead_v1_follow","params":[true]}"# + .to_string(), + ); + + let mut stream = connection.responses(); + let deadline = Instant::now() + Duration::from_secs(60); + let mut initialized_seen = false; + while Instant::now() < deadline { + let Some(frame) = futures::executor::block_on(stream.next()) else { + break; + }; + if frame.contains("\"event\":\"initialized\"") { + initialized_seen = true; + break; + } + } + assert!(initialized_seen, "did not observe initialized event"); + } +} diff --git a/rust/crates/truapi-server/src/smoldot_provider/native_platform.rs b/rust/crates/truapi-server/src/smoldot_provider/native_platform.rs new file mode 100644 index 00000000..ad8bcc1b --- /dev/null +++ b/rust/crates/truapi-server/src/smoldot_provider/native_platform.rs @@ -0,0 +1,16 @@ +//! Native platform wiring for `smoldot_light`. +//! +//! `DefaultPlatform` ships with smoldot-light (std feature, default on) and +//! spawns its own background threads for network I/O. + +use std::sync::Arc; + +use smoldot_light::platform::default::DefaultPlatform; + +/// Alias for the platform reference type smoldot-light expects. +pub type PlatformRefAlias = Arc; + +/// Creates the native smoldot platform implementation used by the server. +pub fn make_platform() -> PlatformRefAlias { + DefaultPlatform::new("truapi".into(), env!("CARGO_PKG_VERSION").into()) +} diff --git a/rust/crates/truapi-server/src/smoldot_provider/specs/asset-hub-paseo.json b/rust/crates/truapi-server/src/smoldot_provider/specs/asset-hub-paseo.json new file mode 100644 index 00000000..7416f3f5 --- /dev/null +++ b/rust/crates/truapi-server/src/smoldot_provider/specs/asset-hub-paseo.json @@ -0,0 +1 @@ +{"name":"Paseo Asset Hub","id":"asset-hub-paseo","chainType":"Live","bootNodes":["/dns/asset-hub-paseo-boot-ng.dwellir.com/tcp/443/wss/p2p/12D3KooWGoC9CdpY8T5bgf6PqKgry2DjCxaqQS7R9WdQ8rVMeEMg","/dns/asset-hub-paseo-boot-ng.dwellir.com/tcp/30357/p2p/12D3KooWGoC9CdpY8T5bgf6PqKgry2DjCxaqQS7R9WdQ8rVMeEMg","/dns/boot-node.helikon.io/tcp/10120/p2p/12D3KooWKrg6Qb2HhTc3onfkgsNbRU72RjNDm96Xh2E7sWso5LZT","/dns/boot.metaspan.io/tcp/36092/p2p/12D3KooWHdtFsjicVWecmiRrc8XLc3A6iLLKKfUwjZXu5mtg1k3w","/dns/boot.metaspan.io/tcp/36096/wss/p2p/12D3KooWHdtFsjicVWecmiRrc8XLc3A6iLLKKfUwjZXu5mtg1k3w","/dns/assethub-paseo-bootnode.radiumblock.com/tcp/30333/p2p/12D3KooWP8aNgAjkYzH1QuwLjYyNqfpWkJkFRgdtUuey9KzEJciq","/dns/assethub-paseo-bootnode.radiumblock.com/tcp/30336/wss/p2p/12D3KooWP8aNgAjkYzH1QuwLjYyNqfpWkJkFRgdtUuey9KzEJciq","/dns/asset-hub-paseo.boot.rotko.net/tcp/34011/p2p/12D3KooWLzC336hvwY7Vyjdwc8VMMMyqnwph1UXMoi1LEbw8RiHj","/dns/asset-hub-paseo.boot.rotko.net/tcp/30435/wss/p2p/12D3KooWLzC336hvwY7Vyjdwc8VMMMyqnwph1UXMoi1LEbw8RiHj","/dns/asset-hub-paseo-bootnode.turboflakes.io/tcp/30330/p2p/12D3KooWJzfVkdDnKfn2hQ1c3ysrbmReTjVKrEBHkdwgZThbB1BM"],"telemetryEndpoints":null,"protocolId":null,"properties":{"ss58Format":0,"tokenDecimals":10,"tokenSymbol":"PAS"},"relay_chain":"paseo","para_id":1000,"codeSubstitutes":{},"genesis":{"stateRootHash":"0xe24abb446f9aa757610fedc53cc631536f3b456a927a1ab1b9018ea2df3c925d"}} diff --git a/rust/crates/truapi-server/src/smoldot_provider/specs/paseo.json b/rust/crates/truapi-server/src/smoldot_provider/specs/paseo.json new file mode 100644 index 00000000..3299c61e --- /dev/null +++ b/rust/crates/truapi-server/src/smoldot_provider/specs/paseo.json @@ -0,0 +1 @@ +{"name":"Paseo Testnet","id":"paseo","chainType":"Live","bootNodes":["/dns/paseo-boot-ng.dwellir.com/tcp/443/wss/p2p/12D3KooWBLLFKDGBxCwq3QmU3YwWKXUx953WwprRshJQicYu4Cfr","/dns/paseo-boot-ng.dwellir.com/tcp/30354/p2p/12D3KooWBLLFKDGBxCwq3QmU3YwWKXUx953WwprRshJQicYu4Cfr","/dns/boot-node.helikon.io/tcp/10020/p2p/12D3KooWBetfzZpf6tGihKrqCo5z854Ub4ZNAUUTRT6eYHNh7FYi","/dns/boot-node.helikon.io/tcp/10022/wss/p2p/12D3KooWBetfzZpf6tGihKrqCo5z854Ub4ZNAUUTRT6eYHNh7FYi","/dns/paseo.bootnodes.polkadotters.com/tcp/30538/p2p/12D3KooWPbbFy4TefEGTRF5eTYhq8LEzc4VAHdNUVCbY4nAnhqPP","/dns/paseo.bootnodes.polkadotters.com/tcp/30540/wss/p2p/12D3KooWPbbFy4TefEGTRF5eTYhq8LEzc4VAHdNUVCbY4nAnhqPP","/dns/paseo.boot.rotko.net/tcp/34001/p2p/12D3KooWRH8eBMhw8c7bucy6pJfy94q4dKpLkF3pmeGohHmemdRu","/dns/paseo-bootnode.turboflakes.io/tcp/30630/p2p/12D3KooWMjCN2CrnN71hAdehn6M2iYKeGdGbZ1A3SKhf4hxrgG9e","/dns/paseo-bootnode.turboflakes.io/tcp/30730/wss/p2p/12D3KooWMjCN2CrnN71hAdehn6M2iYKeGdGbZ1A3SKhf4hxrgG9e"],"telemetryEndpoints":null,"protocolId":"pas","properties":{"ss58Format":0,"tokenDecimals":10,"tokenSymbol":"PAS"},"codeSubstitutes":{},"genesis":{"stateRootHash":"0x2b2a8395a8ec27c54d322d3a6602152da0e3bd0c8f4c01f17a572a44a8e36ab6"},"lightSyncState":{"babeEpochChanges":"0x04341261206265a6436e0e0fd9b525aa8e1fbaac4b17ea7c11a56d55539bc9aa5243c9a30001788b9d1100000000d08d9d11000000000465750ea779998bc4c76d83ff6dc76ab37bd8d92930aad8894c416c2217c561159acba30001d08d9d110000000028909d1100000000044d7e0a8076198d3a12e5f0e597ff16c30477127886b427baef959fdacd9e1e8ef0cda3000128909d110000000080929d110000000000000c341261206265a6436e0e0fd9b525aa8e1fbaac4b17ea7c11a56d55539bc9aa5243c9a300015c46000000000000788b9d1100000000580200000000000065021004c021ae3e2d15f18d90800390836c5b30ca712d2f11c98e6ce41c3398175e010000000000000052fdf822726bb7479e08d00807b958bbc1240cc192c9b181e55437adabb528000100000000000000306c08e74c89dfad418ae01da4e3823ee82d1d74fcc2afd885baf7d43c50236401000000000000004e18d3de91affc85026448233d8c2cbc22d2f9ac49cbc7995e9c81b51a43cc6e010000000000000044454c1621aee6e3c1d9c3fb5026590d26be8add25900a394e24cf4138fc351001000000000000005e6f361a74bebe89866eb73d4fbe8689acfbecfaafeabc40e43236e656f22e39010000000000000094f97236c3431614d5b4deb6122dbd59eb801176dddb0ae30bce8afa1051d50b010000000000000012a9842734de09ea4847d09f1cbbbef40a44ea00733501c8933cf52442463228010000000000000010b5c4551c264f14cb2d5db4d5b50552d69534e013a062a9cb03d766df48ef4e010000000000000088435dc373ebb523a73f6d5e1496dd90627052f7e9369e38bde2581e5a9aca1f01000000000000003a54928c21dbe6d0d8ce229ae899640194f962821a4faa6a911fa8fda1809b7a0100000000000000f84107fd75033c949f96e32c2aaf97f721f46ff5c076fa764ac95500cf9a840b01000000000000005abfafd5b636a5263653f7217d74a02711a4f13962c3f694f875f6d0cf1b357a010000000000000088ed4c0c51838ad383c7554841f76c6f623da39f948110f927bc0701a8c8960f01000000000000007ea9e371e7793f45efbd5818ba2c6b32402df23f653bd3ac8e65ab72424adb1d0100000000000000eabeed18aba996fff73a024926d079fde07fc9612d4de305bf630f7bd34db7510100000000000000dada9cf6148af40fccdda9586e25c50088651f0dcfe6a9a6057dafe4d9ea8e06010000000000000068ba40acfe65dd1daa70dd5c6c3fba664e0cf005e175f0204deb1c5d1d15f454010000000000000050073bf942fbba7324a63e6c174d99240bd649bd5d0d501ca75bd259f2fa34510100000000000000c0d7173ab223397c821c26326e935363b12fe7422323a0c34178630cc2c91d3501000000000000006aa461fba284ea42deadaa35f2c255616cffc121334ee4b3303da5bad29f79340100000000000000daa36dfb87d7ba34e315b4e31388ae6cba278cdb9896c417efa5c2aac97a927b01000000000000009ab2927908f20efe0aa8218bb35584aab8c12ed8296d35ff52fdc09cd31ec744010000000000000044bd6d820dc0b0aa0e896331ff92a49fb13d4c9a1cba4a09cfcdeed6f9b78532010000000000000018fe1ca74d50a35d5c036bc87c52e3b0d5630f7b84ead37963388a5464e0d4490100000000000000e48535befb2e807e3d0cc182fe9e05a2d3a8bac4dec6448aae7eab5fa86956250100000000000000d81837593758987c9ef04f350de913a8a9ceaaefacb1ce4f54e27512e047d0090100000000000000724281a8cd7ff6cf4fb578ca8fc4bc9e41a3c223e10c5f11ec50acace91ef0710100000000000000d8d17c130c3b9281eb825c8020ecfb34823fdcb5b1423a4437e9ba1ff137d67701000000000000002a0cd19f595d69c11038897ee10a39bb8a05b3f254a4649bfa5a4c8b658b540a0100000000000000508b057341d4a6801eff555343dcf7f427b5dc97f711f702e34074821e28d43a0100000000000000b29445ff1e828bbbf47c3f513efee4a5a23366620033ac53b8408d89a50d8517010000000000000080701df791f141b4599be4c08884ec0f204cc05393a2ce635fc30a00e98fe137010000000000000062d30d7886fc9cbb5ce8d48b9df7f5b94906f633a7cc9f6fd3000ef1257aa46501000000000000001c1420b31f9900d967e3dc211dd4a35e446362ecfbf086903ebf420a9e35b77e010000000000000018f7c58fb2cf70d650f445d474d2be3fb9e8cec4a71bccf0effc229a8784df6101000000000000003e149057acee6fe201e2233981c4806aa4e200520d25c8f7f5d42002728fce7401000000000000002cca14fe72f6c3b74b58957566916f051a17835bfdfe68568a05f501f499f87001000000000000009ef6a9bd209d70f858a0eccdbc1cfe4e9438967cd75c395c4552b2d3ce05ab520100000000000000da5e5836d77f80d1196569fd401327b8f59d03251921d4e1740327e39e23da6b010000000000000092ee42aa9270f21adfd9f282ffb73b3e942fc06038bc83de35d8e9971fdde3080100000000000000c8f17d9b568dc9792cd03c61a1342e81afbc1dcd3f5fe54d5a85fd50ad85b6230100000000000000402a56dc42d0874e9494013cbcfa9b8a8e38f36a8a64190a3b3cbef43ac0643e01000000000000002887dcb68a706130ac662baa3021d73f5204b56aad3d87eff0ba7774f18ce54b0100000000000000182e505cd7b3d53b33aa96b6729c182c335026cfe3b16e9b02be3bae64d80d2301000000000000001cf887bde697b016fc21f0bb5cc432a02f1604d7618944bb0a8b332565db2f250100000000000000c2271f77443650b87c58eafb27ef622759ce18eba3f11d5c790d90113b5baa390100000000000000dc58296ce30054fc942543d1058bd81d994ccf73ffc4fe7af1bfaaab1ab4ab180100000000000000289ea60fad4ef35fa3a1743a46d23b7c7530ac4497002b205e6b30ed69604a290100000000000000d8eda667e774930626e2ca3fc676559fa14e5e573fb3d86addf9fcc57bb9ea32010000000000000088f9078eac35965f1868f68f8759ba030ceb7d28bd503b7df9f39fd38e87e25a0100000000000000d23382c75de4f812e29e8895c880793b4ab4fd3d28790944675499e255d4774b0100000000000000660fe756513cbcabaeed97224784cef9f128a4d4b02bb4fa004ed9663feab90f0100000000000000b60d525db7d60ac59d4ccffa3afd8a1094a080729c0d19c7b539b0191c52640101000000000000000c0bcbe2e6ff076aac9f38fed2e301c08447fafc07e085a0980beb3fab0ed526010000000000000060d29f2aa543abe8e1f102a5f106a6496f63298103d9231f730721bc62116e1c01000000000000009a883590ad1970522f048f8ead7b0e6dbb4a586bd068ace45201fada8981c8770100000000000000e6a08e00ddfe7e2158d83bce39035533eca9502c8693d44c2baa472e2ea63d4701000000000000007ee37a4d18261a43aeaa08030dada2fa2fa0b9dfbf4aa39e36973c6b1a2fdb5e010000000000000062d1095d44ec36d6b76f22ef82ac0f75505f722ea0627ae0bfe5e261f503823f010000000000000008f7dded97687143c275e5e947f1b335366a3d207c88df746a0c1c40af33b57e0100000000000000badb0b0033fb149dcb82e55a3a27ccc846c9572c13f88a8fb927e56903106971010000000000000042d092dfc6e035ea66fe617d8523dafec8d46cfe4db19ff22bab8355b483854c0100000000000000a8c0111411f9ae214c425b21a708841db2c094bae35951fe738d806d3349985f01000000000000009e4a15681b6086bede043fcd452d68426aee3afe74a9fccbca015a8d120bb207010000000000000050964893494da29e8824ab45493d956a784bdb31ec4b2311a2906c6ad1bab11e01000000000000001a24a7e0ab1ba604832e179a20f1b221d632f340749cc8a01de7aabcf098ef51010000000000000092f78f633dddc2d47a49b48a6684826c5447afea2ddf50875010298352280d050100000000000000fa26b1a6aac636a1b8d7db8b177f3f7b3915c5b287e86f06dd9015b815512b410100000000000000640077e1a2175b4d734aa5e597c1f28bc09817b9c5d3af37b24314e80da56f4d0100000000000000e007bad84209b4a4ca5110d717b98050ca2d3712b5e2795a991ff62c8e217e2901000000000000007a911bf0ab42098d8db035743bd4f7b7a5ed91a44508837da94e111357e6801c0100000000000000a8f2fa12f1bd4245d7af8390f9fcc9206aed550ff8366bf81412e2501f8b5a0701000000000000008e0b120eea5193f6731daac3fd19cd744191e250592e190797d0c9273912fa1901000000000000003af2e1ce5fc087258447fc497f472912caa1fa0702573884c6020fac3cbb745101000000000000004638b50368236b3da4535ffc55c140f731fb153b9562db753b40fda7cea8c41c0100000000000000926fd4ade255b73ad4a4dd444acd2f4565deb2336223d661d9f1781d9f729426010000000000000016b71bd30e9748c8c89e5898fbf0f01e320fdd574aee590e6cac3ebaf0028c0301000000000000007640ba8105b5391aa56fef680822c673dc422b7d601c02cce4949dcece0ab85a0100000000000000f6850b653753d8a7231fe0f790f2f5fee8530743b41a2f8f4c65677a09144128010000000000000028e5fe1fc01920b017233d4debce9179791c8e60995f950756258c17f3e626150100000000000000123d59ae5331208e9810e9354ba4cb7466ca49d078e9086adb699254ca457d3b01000000000000001aa715bb53e904ba98cfe1bf2908c6ccdc952d6a584ba967e560c94d61275b640100000000000000200c96621fa81f67fb63bd35aa62b28be6d5ffafda169e126bba66332124013e0100000000000000b0df64620c6245803cc675496b3d28150fef6bc966684ca524908dd53bc6605d01000000000000004c677e26e24ecf36659011fbc9ecfb87f98e2192102ba4ec7eb2a307014c28140100000000000000c05be111614164f811b6d792a9a5defb1bd1bfb6712b8f6632719ec47ebcbe3a0100000000000000e6a82cb09111b1828030ebcba68f066cc8a2f0d7032edf102463023a571ed51201000000000000001a52989f498960edd384cdd3f52b11984912325ae290c3050886d009bf036f7b0100000000000000027e4cfaeaae32857c2595dcc4b696790a16c313b193dcc43bfe6ee593dfca650100000000000000cafdce7b198eeede8fbb8e6e44d43ea6aa5c152577a89989002968798c87b737010000000000000054d303a6f6f58c705f3bf9e03bbcfd5fc5e78d77844e0c557782da67a34fd10d0100000000000000e0e9401a2554ae5ed8623a6a0dd4dd8115b0112262cbeac006a4adfd7929ae5001000000000000001422cb0f567ee5f431a7a871f43e8602d7e0b172dd6bf2819ca2a46e6a87c9600100000000000000fc2d047eed5fa2dfb6d7117c177ef29748d93127826e79cf6824ae863dad9a440100000000000000940491aa5c11a7dceac769beea3d4a3815f995b30f995daff3c0410ff3ec4c130100000000000000ea347d23e3bf4f11047d1580444e7f6e2797cfecafa435aace72fbc5b4a51826010000000000000022fc849fcf86c5a7b583c6580597c141c56c3115298d49a0a36d6b7e441b6f52010000000000000088ba9223502f96209814c38bec4994fe2129793f357b07aca4ede940241cd95001000000000000002c016acd6877946eb48b83a1dde11059adb5044428ce43809d3216de6d92240d0100000000000000bec6ff2a5ae2411507a472960fe1297ea1f4a197d4f5346eb71b2f2327a9504801000000000000008eb5892277e6af39c9c440dfc7554ca68b7fb10eeeb9ebc33b24815de6bb90250100000000000000867404b8820644b7ad4d620d7472dbd6b5f761e924cb364651959e06b1975e57010000000000000088da7347f8d84f63a8f499d384aeb6977dd7b840b12ead209abe4c86ab9312650100000000000000f4c75a45902b4aba440cce1947300008a6fdbc6a988fb8647edf915b99fef875010000000000000040319d4152ef99f4dae00d763969f766c3f2421a1c3a213dcad56fefbfb8c35d0100000000000000e01b84f0da8be0b1807c5cb83889bbe5148a1e8a96b0aeb9b4ae15fe674588680100000000000000e6f18b461c86aa7ad1b300ac15d9611011b44c9a10f64cd2938d6bb52be03c120100000000000000be343eab7637aa2890bbf849b345f2540ceb74ebc9a3cc86888a67902bd3e91c010000000000000020089c94aa9b42573c668eb666a7049873132508a078cc3eba7bbab25704100d01000000000000001ef72e689e5d465cb2572fafe50d869d28b219d315c18d2ac9d3f6b8ec610c6e0100000000000000302d97fc1992127356b91159e3a1d9ac27d9ccc65732d124287f1fbfb8e24a7c010000000000000028114b62e53ee6aa11fc6d5cf4c70aad10dc503a0c1d801665c176a9052e354b01000000000000000238f70977ce6294b0eb3c7206469bedf524544db1778a6ba78ae88f4472817a0100000000000000787cdecf233c0f0730da3ddbcf8c5abb0fd501f3ffe2dbd252b5bb031d207c6b0100000000000000765e230454ea87385e7158e57f21dcf67b0922344ea879638e821c51f4d4a00f0100000000000000f8eb06c255edf56235fe6d613042bb2a2a21df3002a453ad34e91d5e00d6ae650100000000000000bce7396a75dc0c6686c24d42ebc720c1fe2e38b5790a60cb90d5bd0da736aa58010000000000000026c8e0e882482190ae9d58ff7c914e881024de60fd7e0f207c83357f30acaf1901000000000000006441ff8df4d2619237e696f8103737f3b003bf66267fb681f7df831021ff441a01000000000000005a8b4fe4cc6ddbb01f141c2565530bc9677e6d6baaa50ad80a529c4e9094f31e0100000000000000ce566e342591b85edcca0c57c4ed252ae80ba9e4298fdfa0c5d984a6bdc72b4401000000000000007e18f0ebfbac50554a56cc4f1bdcf70642d36393f9f0253f55518557b35bfb4c01000000000000005a6da85d909fd6baf57b8cc915cb27b6d8ec6d39247f146aa0109bd59d17d8050100000000000000e8cbd70d0678b15fdc01adcc0a1442836332f30d55d89661ae51191a09ae5a150100000000000000cae6f66d652565c27aa90259bc04296eb4962e6d8ab8b3eee1455174d52f24490100000000000000960f63edec5036689e60e56a0f2eb75f45e4ab5befb36f03b10c988d9eff6a45010000000000000052d5c76808348ba5e350ce32b94b225a7f1c577a8899811a10efe0c18316634d01000000000000000845f4903a8500c8aaae9ea4a7d6d018f4cd148e958e2f723fccebbdc982dd0601000000000000009a8551aa56ddba67f238f2870e995f5e5c14636e72815e8ef671f97ab8debd6b01000000000000008eeaba1502468d954e19f3e28e0aae48196d229a6eb5762b22ea2fe431b5a85101000000000000009c15c7a9ed43f74f816de4ba1c6d78d92bb873ede60ea4c75e9781807157524201000000000000008a0230f16fdb15c9855d211fcbd3a985819a8557ef6907cfdf0b44645e617057010000000000000058b08c94c313efda4bba48778afc50e540702b527d2d583d38f3a83ab639c85c0100000000000000f08c9a0d1e9ea3491c6bbdf94733ddcb07ad16cb75953748946c4b65c1628847010000000000000056854f0d2a2deeb5729c37d65334ad98a193fd29847f92cd7cba0935f33ee8140100000000000000f8fe5e8c5e2173894a49cabe37a9ba068c383043bf90d8694f37373c3d401a200100000000000000c2e8426228531975cb2963d57bff68b31949840187b43a43836e43a247350c470100000000000000408f0da37f48afc23014b52c52b4de90b04cb47f0021500983800f891a9ad0350100000000000000c6e0d3d715fa693dc0fec013c0d58cfeab2698b1da6472076b9ac9aecc3f70350100000000000000022cedafe7d099e6c4c6063b07daed9c9a4f85a14f01a2524a3a66710cff0d1801000000000000001074454308ee7ea6f14915f6292003376a0c4a7f7d47bfdf7c03fee7217e3f0401000000000000000215a230c8a0056fbd42596c7d707fbad5a04d2058ac6e485ea57ab97ec358740100000000000000f087ec6e70b76d03309c82b4e3721a7d23b487c29953a33f6bb1f4faa3930026010000000000000088d889391d92f6ac6d6dcc195599375b4db24e414924690166f8ad1eb7b5b6070100000000000000d86dbadd9c4938544b946d2d2ddb57a963aeb1d4a7f34566edbeab1873ca357c0100000000000000a84b258720551c4c0f1759c6c82316468000547da08a3c0326de3f9d71c353470100000000000000d0f8ecef301c4ed00db8a84c4e2849192f80428adc7703d376572b70136c755c01000000000000001267d8885ac15dd9c3dc1fb28a3604963bd650ca2a99a097e0aa7b56b6a41d2c0100000000000000ec28e2aa5c3b96b46a8082a6cdee58e6b8eec81380b0fc14c9809f750b7250540100000000000000c6e1836fd76d6c06dc9b277d4b0782e8973c0655244d59f9f6712d161f15502201000000000000007a2886724b12f97d1b7ad788067b082286f3cd47f0f701f415214526da8a4d67010000000000000006b7a572d089bcd98935562a8996b4b2f6de5814887c7cad1ccf5cc2d23656330100000000000000afa860847f66ac569cbebb396db7c03c2cff7f4e6021f5c4020822240c5b59ab01000000000000000400000000000000024d7e0a8076198d3a12e5f0e597ff16c30477127886b427baef959fdacd9e1e8ef0cda300015e4600000000000028909d1100000000580200000000000065021004c021ae3e2d15f18d90800390836c5b30ca712d2f11c98e6ce41c3398175e010000000000000052fdf822726bb7479e08d00807b958bbc1240cc192c9b181e55437adabb528000100000000000000306c08e74c89dfad418ae01da4e3823ee82d1d74fcc2afd885baf7d43c50236401000000000000004e18d3de91affc85026448233d8c2cbc22d2f9ac49cbc7995e9c81b51a43cc6e010000000000000044454c1621aee6e3c1d9c3fb5026590d26be8add25900a394e24cf4138fc351001000000000000005e6f361a74bebe89866eb73d4fbe8689acfbecfaafeabc40e43236e656f22e39010000000000000094f97236c3431614d5b4deb6122dbd59eb801176dddb0ae30bce8afa1051d50b010000000000000012a9842734de09ea4847d09f1cbbbef40a44ea00733501c8933cf52442463228010000000000000010b5c4551c264f14cb2d5db4d5b50552d69534e013a062a9cb03d766df48ef4e010000000000000088435dc373ebb523a73f6d5e1496dd90627052f7e9369e38bde2581e5a9aca1f01000000000000003a54928c21dbe6d0d8ce229ae899640194f962821a4faa6a911fa8fda1809b7a0100000000000000f84107fd75033c949f96e32c2aaf97f721f46ff5c076fa764ac95500cf9a840b01000000000000005abfafd5b636a5263653f7217d74a02711a4f13962c3f694f875f6d0cf1b357a010000000000000088ed4c0c51838ad383c7554841f76c6f623da39f948110f927bc0701a8c8960f01000000000000007ea9e371e7793f45efbd5818ba2c6b32402df23f653bd3ac8e65ab72424adb1d0100000000000000eabeed18aba996fff73a024926d079fde07fc9612d4de305bf630f7bd34db7510100000000000000dada9cf6148af40fccdda9586e25c50088651f0dcfe6a9a6057dafe4d9ea8e06010000000000000068ba40acfe65dd1daa70dd5c6c3fba664e0cf005e175f0204deb1c5d1d15f454010000000000000050073bf942fbba7324a63e6c174d99240bd649bd5d0d501ca75bd259f2fa34510100000000000000c0d7173ab223397c821c26326e935363b12fe7422323a0c34178630cc2c91d3501000000000000006aa461fba284ea42deadaa35f2c255616cffc121334ee4b3303da5bad29f79340100000000000000daa36dfb87d7ba34e315b4e31388ae6cba278cdb9896c417efa5c2aac97a927b01000000000000009ab2927908f20efe0aa8218bb35584aab8c12ed8296d35ff52fdc09cd31ec744010000000000000044bd6d820dc0b0aa0e896331ff92a49fb13d4c9a1cba4a09cfcdeed6f9b78532010000000000000018fe1ca74d50a35d5c036bc87c52e3b0d5630f7b84ead37963388a5464e0d4490100000000000000e48535befb2e807e3d0cc182fe9e05a2d3a8bac4dec6448aae7eab5fa86956250100000000000000d81837593758987c9ef04f350de913a8a9ceaaefacb1ce4f54e27512e047d0090100000000000000724281a8cd7ff6cf4fb578ca8fc4bc9e41a3c223e10c5f11ec50acace91ef0710100000000000000d8d17c130c3b9281eb825c8020ecfb34823fdcb5b1423a4437e9ba1ff137d67701000000000000002a0cd19f595d69c11038897ee10a39bb8a05b3f254a4649bfa5a4c8b658b540a0100000000000000508b057341d4a6801eff555343dcf7f427b5dc97f711f702e34074821e28d43a0100000000000000b29445ff1e828bbbf47c3f513efee4a5a23366620033ac53b8408d89a50d8517010000000000000080701df791f141b4599be4c08884ec0f204cc05393a2ce635fc30a00e98fe137010000000000000062d30d7886fc9cbb5ce8d48b9df7f5b94906f633a7cc9f6fd3000ef1257aa46501000000000000001c1420b31f9900d967e3dc211dd4a35e446362ecfbf086903ebf420a9e35b77e010000000000000018f7c58fb2cf70d650f445d474d2be3fb9e8cec4a71bccf0effc229a8784df6101000000000000003e149057acee6fe201e2233981c4806aa4e200520d25c8f7f5d42002728fce7401000000000000002cca14fe72f6c3b74b58957566916f051a17835bfdfe68568a05f501f499f87001000000000000009ef6a9bd209d70f858a0eccdbc1cfe4e9438967cd75c395c4552b2d3ce05ab520100000000000000da5e5836d77f80d1196569fd401327b8f59d03251921d4e1740327e39e23da6b010000000000000092ee42aa9270f21adfd9f282ffb73b3e942fc06038bc83de35d8e9971fdde3080100000000000000c8f17d9b568dc9792cd03c61a1342e81afbc1dcd3f5fe54d5a85fd50ad85b6230100000000000000402a56dc42d0874e9494013cbcfa9b8a8e38f36a8a64190a3b3cbef43ac0643e01000000000000002887dcb68a706130ac662baa3021d73f5204b56aad3d87eff0ba7774f18ce54b0100000000000000182e505cd7b3d53b33aa96b6729c182c335026cfe3b16e9b02be3bae64d80d2301000000000000001cf887bde697b016fc21f0bb5cc432a02f1604d7618944bb0a8b332565db2f250100000000000000c2271f77443650b87c58eafb27ef622759ce18eba3f11d5c790d90113b5baa390100000000000000dc58296ce30054fc942543d1058bd81d994ccf73ffc4fe7af1bfaaab1ab4ab180100000000000000289ea60fad4ef35fa3a1743a46d23b7c7530ac4497002b205e6b30ed69604a290100000000000000d8eda667e774930626e2ca3fc676559fa14e5e573fb3d86addf9fcc57bb9ea32010000000000000088f9078eac35965f1868f68f8759ba030ceb7d28bd503b7df9f39fd38e87e25a0100000000000000d23382c75de4f812e29e8895c880793b4ab4fd3d28790944675499e255d4774b0100000000000000660fe756513cbcabaeed97224784cef9f128a4d4b02bb4fa004ed9663feab90f0100000000000000b60d525db7d60ac59d4ccffa3afd8a1094a080729c0d19c7b539b0191c52640101000000000000000c0bcbe2e6ff076aac9f38fed2e301c08447fafc07e085a0980beb3fab0ed526010000000000000060d29f2aa543abe8e1f102a5f106a6496f63298103d9231f730721bc62116e1c01000000000000009a883590ad1970522f048f8ead7b0e6dbb4a586bd068ace45201fada8981c8770100000000000000e6a08e00ddfe7e2158d83bce39035533eca9502c8693d44c2baa472e2ea63d4701000000000000007ee37a4d18261a43aeaa08030dada2fa2fa0b9dfbf4aa39e36973c6b1a2fdb5e010000000000000062d1095d44ec36d6b76f22ef82ac0f75505f722ea0627ae0bfe5e261f503823f010000000000000008f7dded97687143c275e5e947f1b335366a3d207c88df746a0c1c40af33b57e0100000000000000badb0b0033fb149dcb82e55a3a27ccc846c9572c13f88a8fb927e56903106971010000000000000042d092dfc6e035ea66fe617d8523dafec8d46cfe4db19ff22bab8355b483854c0100000000000000a8c0111411f9ae214c425b21a708841db2c094bae35951fe738d806d3349985f01000000000000009e4a15681b6086bede043fcd452d68426aee3afe74a9fccbca015a8d120bb207010000000000000050964893494da29e8824ab45493d956a784bdb31ec4b2311a2906c6ad1bab11e01000000000000001a24a7e0ab1ba604832e179a20f1b221d632f340749cc8a01de7aabcf098ef51010000000000000092f78f633dddc2d47a49b48a6684826c5447afea2ddf50875010298352280d050100000000000000fa26b1a6aac636a1b8d7db8b177f3f7b3915c5b287e86f06dd9015b815512b410100000000000000640077e1a2175b4d734aa5e597c1f28bc09817b9c5d3af37b24314e80da56f4d0100000000000000e007bad84209b4a4ca5110d717b98050ca2d3712b5e2795a991ff62c8e217e2901000000000000007a911bf0ab42098d8db035743bd4f7b7a5ed91a44508837da94e111357e6801c0100000000000000a8f2fa12f1bd4245d7af8390f9fcc9206aed550ff8366bf81412e2501f8b5a0701000000000000008e0b120eea5193f6731daac3fd19cd744191e250592e190797d0c9273912fa1901000000000000003af2e1ce5fc087258447fc497f472912caa1fa0702573884c6020fac3cbb745101000000000000004638b50368236b3da4535ffc55c140f731fb153b9562db753b40fda7cea8c41c0100000000000000926fd4ade255b73ad4a4dd444acd2f4565deb2336223d661d9f1781d9f729426010000000000000016b71bd30e9748c8c89e5898fbf0f01e320fdd574aee590e6cac3ebaf0028c0301000000000000007640ba8105b5391aa56fef680822c673dc422b7d601c02cce4949dcece0ab85a0100000000000000f6850b653753d8a7231fe0f790f2f5fee8530743b41a2f8f4c65677a09144128010000000000000028e5fe1fc01920b017233d4debce9179791c8e60995f950756258c17f3e626150100000000000000123d59ae5331208e9810e9354ba4cb7466ca49d078e9086adb699254ca457d3b01000000000000001aa715bb53e904ba98cfe1bf2908c6ccdc952d6a584ba967e560c94d61275b640100000000000000200c96621fa81f67fb63bd35aa62b28be6d5ffafda169e126bba66332124013e0100000000000000b0df64620c6245803cc675496b3d28150fef6bc966684ca524908dd53bc6605d01000000000000004c677e26e24ecf36659011fbc9ecfb87f98e2192102ba4ec7eb2a307014c28140100000000000000c05be111614164f811b6d792a9a5defb1bd1bfb6712b8f6632719ec47ebcbe3a0100000000000000e6a82cb09111b1828030ebcba68f066cc8a2f0d7032edf102463023a571ed51201000000000000001a52989f498960edd384cdd3f52b11984912325ae290c3050886d009bf036f7b0100000000000000027e4cfaeaae32857c2595dcc4b696790a16c313b193dcc43bfe6ee593dfca650100000000000000cafdce7b198eeede8fbb8e6e44d43ea6aa5c152577a89989002968798c87b737010000000000000054d303a6f6f58c705f3bf9e03bbcfd5fc5e78d77844e0c557782da67a34fd10d0100000000000000e0e9401a2554ae5ed8623a6a0dd4dd8115b0112262cbeac006a4adfd7929ae5001000000000000001422cb0f567ee5f431a7a871f43e8602d7e0b172dd6bf2819ca2a46e6a87c9600100000000000000fc2d047eed5fa2dfb6d7117c177ef29748d93127826e79cf6824ae863dad9a440100000000000000940491aa5c11a7dceac769beea3d4a3815f995b30f995daff3c0410ff3ec4c130100000000000000ea347d23e3bf4f11047d1580444e7f6e2797cfecafa435aace72fbc5b4a51826010000000000000022fc849fcf86c5a7b583c6580597c141c56c3115298d49a0a36d6b7e441b6f52010000000000000088ba9223502f96209814c38bec4994fe2129793f357b07aca4ede940241cd95001000000000000002c016acd6877946eb48b83a1dde11059adb5044428ce43809d3216de6d92240d0100000000000000bec6ff2a5ae2411507a472960fe1297ea1f4a197d4f5346eb71b2f2327a9504801000000000000008eb5892277e6af39c9c440dfc7554ca68b7fb10eeeb9ebc33b24815de6bb90250100000000000000867404b8820644b7ad4d620d7472dbd6b5f761e924cb364651959e06b1975e57010000000000000088da7347f8d84f63a8f499d384aeb6977dd7b840b12ead209abe4c86ab9312650100000000000000f4c75a45902b4aba440cce1947300008a6fdbc6a988fb8647edf915b99fef875010000000000000040319d4152ef99f4dae00d763969f766c3f2421a1c3a213dcad56fefbfb8c35d0100000000000000e01b84f0da8be0b1807c5cb83889bbe5148a1e8a96b0aeb9b4ae15fe674588680100000000000000e6f18b461c86aa7ad1b300ac15d9611011b44c9a10f64cd2938d6bb52be03c120100000000000000be343eab7637aa2890bbf849b345f2540ceb74ebc9a3cc86888a67902bd3e91c010000000000000020089c94aa9b42573c668eb666a7049873132508a078cc3eba7bbab25704100d01000000000000001ef72e689e5d465cb2572fafe50d869d28b219d315c18d2ac9d3f6b8ec610c6e0100000000000000302d97fc1992127356b91159e3a1d9ac27d9ccc65732d124287f1fbfb8e24a7c010000000000000028114b62e53ee6aa11fc6d5cf4c70aad10dc503a0c1d801665c176a9052e354b01000000000000000238f70977ce6294b0eb3c7206469bedf524544db1778a6ba78ae88f4472817a0100000000000000787cdecf233c0f0730da3ddbcf8c5abb0fd501f3ffe2dbd252b5bb031d207c6b0100000000000000765e230454ea87385e7158e57f21dcf67b0922344ea879638e821c51f4d4a00f0100000000000000f8eb06c255edf56235fe6d613042bb2a2a21df3002a453ad34e91d5e00d6ae650100000000000000bce7396a75dc0c6686c24d42ebc720c1fe2e38b5790a60cb90d5bd0da736aa58010000000000000026c8e0e882482190ae9d58ff7c914e881024de60fd7e0f207c83357f30acaf1901000000000000006441ff8df4d2619237e696f8103737f3b003bf66267fb681f7df831021ff441a01000000000000005a8b4fe4cc6ddbb01f141c2565530bc9677e6d6baaa50ad80a529c4e9094f31e0100000000000000ce566e342591b85edcca0c57c4ed252ae80ba9e4298fdfa0c5d984a6bdc72b4401000000000000007e18f0ebfbac50554a56cc4f1bdcf70642d36393f9f0253f55518557b35bfb4c01000000000000005a6da85d909fd6baf57b8cc915cb27b6d8ec6d39247f146aa0109bd59d17d8050100000000000000e8cbd70d0678b15fdc01adcc0a1442836332f30d55d89661ae51191a09ae5a150100000000000000cae6f66d652565c27aa90259bc04296eb4962e6d8ab8b3eee1455174d52f24490100000000000000960f63edec5036689e60e56a0f2eb75f45e4ab5befb36f03b10c988d9eff6a45010000000000000052d5c76808348ba5e350ce32b94b225a7f1c577a8899811a10efe0c18316634d01000000000000000845f4903a8500c8aaae9ea4a7d6d018f4cd148e958e2f723fccebbdc982dd0601000000000000009a8551aa56ddba67f238f2870e995f5e5c14636e72815e8ef671f97ab8debd6b01000000000000008eeaba1502468d954e19f3e28e0aae48196d229a6eb5762b22ea2fe431b5a85101000000000000009c15c7a9ed43f74f816de4ba1c6d78d92bb873ede60ea4c75e9781807157524201000000000000008a0230f16fdb15c9855d211fcbd3a985819a8557ef6907cfdf0b44645e617057010000000000000058b08c94c313efda4bba48778afc50e540702b527d2d583d38f3a83ab639c85c0100000000000000f08c9a0d1e9ea3491c6bbdf94733ddcb07ad16cb75953748946c4b65c1628847010000000000000056854f0d2a2deeb5729c37d65334ad98a193fd29847f92cd7cba0935f33ee8140100000000000000f8fe5e8c5e2173894a49cabe37a9ba068c383043bf90d8694f37373c3d401a200100000000000000c2e8426228531975cb2963d57bff68b31949840187b43a43836e43a247350c470100000000000000408f0da37f48afc23014b52c52b4de90b04cb47f0021500983800f891a9ad0350100000000000000c6e0d3d715fa693dc0fec013c0d58cfeab2698b1da6472076b9ac9aecc3f70350100000000000000022cedafe7d099e6c4c6063b07daed9c9a4f85a14f01a2524a3a66710cff0d1801000000000000001074454308ee7ea6f14915f6292003376a0c4a7f7d47bfdf7c03fee7217e3f0401000000000000000215a230c8a0056fbd42596c7d707fbad5a04d2058ac6e485ea57ab97ec358740100000000000000f087ec6e70b76d03309c82b4e3721a7d23b487c29953a33f6bb1f4faa3930026010000000000000088d889391d92f6ac6d6dcc195599375b4db24e414924690166f8ad1eb7b5b6070100000000000000d86dbadd9c4938544b946d2d2ddb57a963aeb1d4a7f34566edbeab1873ca357c0100000000000000a84b258720551c4c0f1759c6c82316468000547da08a3c0326de3f9d71c353470100000000000000d0f8ecef301c4ed00db8a84c4e2849192f80428adc7703d376572b70136c755c01000000000000001267d8885ac15dd9c3dc1fb28a3604963bd650ca2a99a097e0aa7b56b6a41d2c0100000000000000ec28e2aa5c3b96b46a8082a6cdee58e6b8eec81380b0fc14c9809f750b7250540100000000000000c6e1836fd76d6c06dc9b277d4b0782e8973c0655244d59f9f6712d161f15502201000000000000007a2886724b12f97d1b7ad788067b082286f3cd47f0f701f415214526da8a4d67010000000000000006b7a572d089bcd98935562a8996b4b2f6de5814887c7cad1ccf5cc2d236563301000000000000009842ae49c1e72c4db0abd83e237d5e1af73d0f4e425134584ef6b8835125db8c010000000000000004000000000000000265750ea779998bc4c76d83ff6dc76ab37bd8d92930aad8894c416c2217c561159acba300015d46000000000000d08d9d1100000000580200000000000065021004c021ae3e2d15f18d90800390836c5b30ca712d2f11c98e6ce41c3398175e010000000000000052fdf822726bb7479e08d00807b958bbc1240cc192c9b181e55437adabb528000100000000000000306c08e74c89dfad418ae01da4e3823ee82d1d74fcc2afd885baf7d43c50236401000000000000004e18d3de91affc85026448233d8c2cbc22d2f9ac49cbc7995e9c81b51a43cc6e010000000000000044454c1621aee6e3c1d9c3fb5026590d26be8add25900a394e24cf4138fc351001000000000000005e6f361a74bebe89866eb73d4fbe8689acfbecfaafeabc40e43236e656f22e39010000000000000094f97236c3431614d5b4deb6122dbd59eb801176dddb0ae30bce8afa1051d50b010000000000000012a9842734de09ea4847d09f1cbbbef40a44ea00733501c8933cf52442463228010000000000000010b5c4551c264f14cb2d5db4d5b50552d69534e013a062a9cb03d766df48ef4e010000000000000088435dc373ebb523a73f6d5e1496dd90627052f7e9369e38bde2581e5a9aca1f01000000000000003a54928c21dbe6d0d8ce229ae899640194f962821a4faa6a911fa8fda1809b7a0100000000000000f84107fd75033c949f96e32c2aaf97f721f46ff5c076fa764ac95500cf9a840b01000000000000005abfafd5b636a5263653f7217d74a02711a4f13962c3f694f875f6d0cf1b357a010000000000000088ed4c0c51838ad383c7554841f76c6f623da39f948110f927bc0701a8c8960f01000000000000007ea9e371e7793f45efbd5818ba2c6b32402df23f653bd3ac8e65ab72424adb1d0100000000000000eabeed18aba996fff73a024926d079fde07fc9612d4de305bf630f7bd34db7510100000000000000dada9cf6148af40fccdda9586e25c50088651f0dcfe6a9a6057dafe4d9ea8e06010000000000000068ba40acfe65dd1daa70dd5c6c3fba664e0cf005e175f0204deb1c5d1d15f454010000000000000050073bf942fbba7324a63e6c174d99240bd649bd5d0d501ca75bd259f2fa34510100000000000000c0d7173ab223397c821c26326e935363b12fe7422323a0c34178630cc2c91d3501000000000000006aa461fba284ea42deadaa35f2c255616cffc121334ee4b3303da5bad29f79340100000000000000daa36dfb87d7ba34e315b4e31388ae6cba278cdb9896c417efa5c2aac97a927b01000000000000009ab2927908f20efe0aa8218bb35584aab8c12ed8296d35ff52fdc09cd31ec744010000000000000044bd6d820dc0b0aa0e896331ff92a49fb13d4c9a1cba4a09cfcdeed6f9b78532010000000000000018fe1ca74d50a35d5c036bc87c52e3b0d5630f7b84ead37963388a5464e0d4490100000000000000e48535befb2e807e3d0cc182fe9e05a2d3a8bac4dec6448aae7eab5fa86956250100000000000000d81837593758987c9ef04f350de913a8a9ceaaefacb1ce4f54e27512e047d0090100000000000000724281a8cd7ff6cf4fb578ca8fc4bc9e41a3c223e10c5f11ec50acace91ef0710100000000000000d8d17c130c3b9281eb825c8020ecfb34823fdcb5b1423a4437e9ba1ff137d67701000000000000002a0cd19f595d69c11038897ee10a39bb8a05b3f254a4649bfa5a4c8b658b540a0100000000000000508b057341d4a6801eff555343dcf7f427b5dc97f711f702e34074821e28d43a0100000000000000b29445ff1e828bbbf47c3f513efee4a5a23366620033ac53b8408d89a50d8517010000000000000080701df791f141b4599be4c08884ec0f204cc05393a2ce635fc30a00e98fe137010000000000000062d30d7886fc9cbb5ce8d48b9df7f5b94906f633a7cc9f6fd3000ef1257aa46501000000000000001c1420b31f9900d967e3dc211dd4a35e446362ecfbf086903ebf420a9e35b77e010000000000000018f7c58fb2cf70d650f445d474d2be3fb9e8cec4a71bccf0effc229a8784df6101000000000000003e149057acee6fe201e2233981c4806aa4e200520d25c8f7f5d42002728fce7401000000000000002cca14fe72f6c3b74b58957566916f051a17835bfdfe68568a05f501f499f87001000000000000009ef6a9bd209d70f858a0eccdbc1cfe4e9438967cd75c395c4552b2d3ce05ab520100000000000000da5e5836d77f80d1196569fd401327b8f59d03251921d4e1740327e39e23da6b010000000000000092ee42aa9270f21adfd9f282ffb73b3e942fc06038bc83de35d8e9971fdde3080100000000000000c8f17d9b568dc9792cd03c61a1342e81afbc1dcd3f5fe54d5a85fd50ad85b6230100000000000000402a56dc42d0874e9494013cbcfa9b8a8e38f36a8a64190a3b3cbef43ac0643e01000000000000002887dcb68a706130ac662baa3021d73f5204b56aad3d87eff0ba7774f18ce54b0100000000000000182e505cd7b3d53b33aa96b6729c182c335026cfe3b16e9b02be3bae64d80d2301000000000000001cf887bde697b016fc21f0bb5cc432a02f1604d7618944bb0a8b332565db2f250100000000000000c2271f77443650b87c58eafb27ef622759ce18eba3f11d5c790d90113b5baa390100000000000000dc58296ce30054fc942543d1058bd81d994ccf73ffc4fe7af1bfaaab1ab4ab180100000000000000289ea60fad4ef35fa3a1743a46d23b7c7530ac4497002b205e6b30ed69604a290100000000000000d8eda667e774930626e2ca3fc676559fa14e5e573fb3d86addf9fcc57bb9ea32010000000000000088f9078eac35965f1868f68f8759ba030ceb7d28bd503b7df9f39fd38e87e25a0100000000000000d23382c75de4f812e29e8895c880793b4ab4fd3d28790944675499e255d4774b0100000000000000660fe756513cbcabaeed97224784cef9f128a4d4b02bb4fa004ed9663feab90f0100000000000000b60d525db7d60ac59d4ccffa3afd8a1094a080729c0d19c7b539b0191c52640101000000000000000c0bcbe2e6ff076aac9f38fed2e301c08447fafc07e085a0980beb3fab0ed526010000000000000060d29f2aa543abe8e1f102a5f106a6496f63298103d9231f730721bc62116e1c01000000000000009a883590ad1970522f048f8ead7b0e6dbb4a586bd068ace45201fada8981c8770100000000000000e6a08e00ddfe7e2158d83bce39035533eca9502c8693d44c2baa472e2ea63d4701000000000000007ee37a4d18261a43aeaa08030dada2fa2fa0b9dfbf4aa39e36973c6b1a2fdb5e010000000000000062d1095d44ec36d6b76f22ef82ac0f75505f722ea0627ae0bfe5e261f503823f010000000000000008f7dded97687143c275e5e947f1b335366a3d207c88df746a0c1c40af33b57e0100000000000000badb0b0033fb149dcb82e55a3a27ccc846c9572c13f88a8fb927e56903106971010000000000000042d092dfc6e035ea66fe617d8523dafec8d46cfe4db19ff22bab8355b483854c0100000000000000a8c0111411f9ae214c425b21a708841db2c094bae35951fe738d806d3349985f01000000000000009e4a15681b6086bede043fcd452d68426aee3afe74a9fccbca015a8d120bb207010000000000000050964893494da29e8824ab45493d956a784bdb31ec4b2311a2906c6ad1bab11e01000000000000001a24a7e0ab1ba604832e179a20f1b221d632f340749cc8a01de7aabcf098ef51010000000000000092f78f633dddc2d47a49b48a6684826c5447afea2ddf50875010298352280d050100000000000000fa26b1a6aac636a1b8d7db8b177f3f7b3915c5b287e86f06dd9015b815512b410100000000000000640077e1a2175b4d734aa5e597c1f28bc09817b9c5d3af37b24314e80da56f4d0100000000000000e007bad84209b4a4ca5110d717b98050ca2d3712b5e2795a991ff62c8e217e2901000000000000007a911bf0ab42098d8db035743bd4f7b7a5ed91a44508837da94e111357e6801c0100000000000000a8f2fa12f1bd4245d7af8390f9fcc9206aed550ff8366bf81412e2501f8b5a0701000000000000008e0b120eea5193f6731daac3fd19cd744191e250592e190797d0c9273912fa1901000000000000003af2e1ce5fc087258447fc497f472912caa1fa0702573884c6020fac3cbb745101000000000000004638b50368236b3da4535ffc55c140f731fb153b9562db753b40fda7cea8c41c0100000000000000926fd4ade255b73ad4a4dd444acd2f4565deb2336223d661d9f1781d9f729426010000000000000016b71bd30e9748c8c89e5898fbf0f01e320fdd574aee590e6cac3ebaf0028c0301000000000000007640ba8105b5391aa56fef680822c673dc422b7d601c02cce4949dcece0ab85a0100000000000000f6850b653753d8a7231fe0f790f2f5fee8530743b41a2f8f4c65677a09144128010000000000000028e5fe1fc01920b017233d4debce9179791c8e60995f950756258c17f3e626150100000000000000123d59ae5331208e9810e9354ba4cb7466ca49d078e9086adb699254ca457d3b01000000000000001aa715bb53e904ba98cfe1bf2908c6ccdc952d6a584ba967e560c94d61275b640100000000000000200c96621fa81f67fb63bd35aa62b28be6d5ffafda169e126bba66332124013e0100000000000000b0df64620c6245803cc675496b3d28150fef6bc966684ca524908dd53bc6605d01000000000000004c677e26e24ecf36659011fbc9ecfb87f98e2192102ba4ec7eb2a307014c28140100000000000000c05be111614164f811b6d792a9a5defb1bd1bfb6712b8f6632719ec47ebcbe3a0100000000000000e6a82cb09111b1828030ebcba68f066cc8a2f0d7032edf102463023a571ed51201000000000000001a52989f498960edd384cdd3f52b11984912325ae290c3050886d009bf036f7b0100000000000000027e4cfaeaae32857c2595dcc4b696790a16c313b193dcc43bfe6ee593dfca650100000000000000cafdce7b198eeede8fbb8e6e44d43ea6aa5c152577a89989002968798c87b737010000000000000054d303a6f6f58c705f3bf9e03bbcfd5fc5e78d77844e0c557782da67a34fd10d0100000000000000e0e9401a2554ae5ed8623a6a0dd4dd8115b0112262cbeac006a4adfd7929ae5001000000000000001422cb0f567ee5f431a7a871f43e8602d7e0b172dd6bf2819ca2a46e6a87c9600100000000000000fc2d047eed5fa2dfb6d7117c177ef29748d93127826e79cf6824ae863dad9a440100000000000000940491aa5c11a7dceac769beea3d4a3815f995b30f995daff3c0410ff3ec4c130100000000000000ea347d23e3bf4f11047d1580444e7f6e2797cfecafa435aace72fbc5b4a51826010000000000000022fc849fcf86c5a7b583c6580597c141c56c3115298d49a0a36d6b7e441b6f52010000000000000088ba9223502f96209814c38bec4994fe2129793f357b07aca4ede940241cd95001000000000000002c016acd6877946eb48b83a1dde11059adb5044428ce43809d3216de6d92240d0100000000000000bec6ff2a5ae2411507a472960fe1297ea1f4a197d4f5346eb71b2f2327a9504801000000000000008eb5892277e6af39c9c440dfc7554ca68b7fb10eeeb9ebc33b24815de6bb90250100000000000000867404b8820644b7ad4d620d7472dbd6b5f761e924cb364651959e06b1975e57010000000000000088da7347f8d84f63a8f499d384aeb6977dd7b840b12ead209abe4c86ab9312650100000000000000f4c75a45902b4aba440cce1947300008a6fdbc6a988fb8647edf915b99fef875010000000000000040319d4152ef99f4dae00d763969f766c3f2421a1c3a213dcad56fefbfb8c35d0100000000000000e01b84f0da8be0b1807c5cb83889bbe5148a1e8a96b0aeb9b4ae15fe674588680100000000000000e6f18b461c86aa7ad1b300ac15d9611011b44c9a10f64cd2938d6bb52be03c120100000000000000be343eab7637aa2890bbf849b345f2540ceb74ebc9a3cc86888a67902bd3e91c010000000000000020089c94aa9b42573c668eb666a7049873132508a078cc3eba7bbab25704100d01000000000000001ef72e689e5d465cb2572fafe50d869d28b219d315c18d2ac9d3f6b8ec610c6e0100000000000000302d97fc1992127356b91159e3a1d9ac27d9ccc65732d124287f1fbfb8e24a7c010000000000000028114b62e53ee6aa11fc6d5cf4c70aad10dc503a0c1d801665c176a9052e354b01000000000000000238f70977ce6294b0eb3c7206469bedf524544db1778a6ba78ae88f4472817a0100000000000000787cdecf233c0f0730da3ddbcf8c5abb0fd501f3ffe2dbd252b5bb031d207c6b0100000000000000765e230454ea87385e7158e57f21dcf67b0922344ea879638e821c51f4d4a00f0100000000000000f8eb06c255edf56235fe6d613042bb2a2a21df3002a453ad34e91d5e00d6ae650100000000000000bce7396a75dc0c6686c24d42ebc720c1fe2e38b5790a60cb90d5bd0da736aa58010000000000000026c8e0e882482190ae9d58ff7c914e881024de60fd7e0f207c83357f30acaf1901000000000000006441ff8df4d2619237e696f8103737f3b003bf66267fb681f7df831021ff441a01000000000000005a8b4fe4cc6ddbb01f141c2565530bc9677e6d6baaa50ad80a529c4e9094f31e0100000000000000ce566e342591b85edcca0c57c4ed252ae80ba9e4298fdfa0c5d984a6bdc72b4401000000000000007e18f0ebfbac50554a56cc4f1bdcf70642d36393f9f0253f55518557b35bfb4c01000000000000005a6da85d909fd6baf57b8cc915cb27b6d8ec6d39247f146aa0109bd59d17d8050100000000000000e8cbd70d0678b15fdc01adcc0a1442836332f30d55d89661ae51191a09ae5a150100000000000000cae6f66d652565c27aa90259bc04296eb4962e6d8ab8b3eee1455174d52f24490100000000000000960f63edec5036689e60e56a0f2eb75f45e4ab5befb36f03b10c988d9eff6a45010000000000000052d5c76808348ba5e350ce32b94b225a7f1c577a8899811a10efe0c18316634d01000000000000000845f4903a8500c8aaae9ea4a7d6d018f4cd148e958e2f723fccebbdc982dd0601000000000000009a8551aa56ddba67f238f2870e995f5e5c14636e72815e8ef671f97ab8debd6b01000000000000008eeaba1502468d954e19f3e28e0aae48196d229a6eb5762b22ea2fe431b5a85101000000000000009c15c7a9ed43f74f816de4ba1c6d78d92bb873ede60ea4c75e9781807157524201000000000000008a0230f16fdb15c9855d211fcbd3a985819a8557ef6907cfdf0b44645e617057010000000000000058b08c94c313efda4bba48778afc50e540702b527d2d583d38f3a83ab639c85c0100000000000000f08c9a0d1e9ea3491c6bbdf94733ddcb07ad16cb75953748946c4b65c1628847010000000000000056854f0d2a2deeb5729c37d65334ad98a193fd29847f92cd7cba0935f33ee8140100000000000000f8fe5e8c5e2173894a49cabe37a9ba068c383043bf90d8694f37373c3d401a200100000000000000c2e8426228531975cb2963d57bff68b31949840187b43a43836e43a247350c470100000000000000408f0da37f48afc23014b52c52b4de90b04cb47f0021500983800f891a9ad0350100000000000000c6e0d3d715fa693dc0fec013c0d58cfeab2698b1da6472076b9ac9aecc3f70350100000000000000022cedafe7d099e6c4c6063b07daed9c9a4f85a14f01a2524a3a66710cff0d1801000000000000001074454308ee7ea6f14915f6292003376a0c4a7f7d47bfdf7c03fee7217e3f0401000000000000000215a230c8a0056fbd42596c7d707fbad5a04d2058ac6e485ea57ab97ec358740100000000000000f087ec6e70b76d03309c82b4e3721a7d23b487c29953a33f6bb1f4faa3930026010000000000000088d889391d92f6ac6d6dcc195599375b4db24e414924690166f8ad1eb7b5b6070100000000000000d86dbadd9c4938544b946d2d2ddb57a963aeb1d4a7f34566edbeab1873ca357c0100000000000000a84b258720551c4c0f1759c6c82316468000547da08a3c0326de3f9d71c353470100000000000000d0f8ecef301c4ed00db8a84c4e2849192f80428adc7703d376572b70136c755c01000000000000001267d8885ac15dd9c3dc1fb28a3604963bd650ca2a99a097e0aa7b56b6a41d2c0100000000000000ec28e2aa5c3b96b46a8082a6cdee58e6b8eec81380b0fc14c9809f750b7250540100000000000000c6e1836fd76d6c06dc9b277d4b0782e8973c0655244d59f9f6712d161f15502201000000000000007a2886724b12f97d1b7ad788067b082286f3cd47f0f701f415214526da8a4d67010000000000000006b7a572d089bcd98935562a8996b4b2f6de5814887c7cad1ccf5cc2d236563301000000000000009f43256f5a7f6f7001828384e2dac9f871cae5928f85d1c6caa7ce84485cc1850100000000000000040000000000000002","babeFinalizedBlockWeight":2681601,"finalizedBlockHeader":"0x68d801d1e18ca6e3e12642c5ccc38ce1ded375d2141a5a8f3ae03f60c77bf790363f8f02c5d35ff0ddcbe5e1358b1826bca42863a0bf303c4c8cca366ab3aab6fc5aa15ed9e2b35427d7b690880d467c26e54d486743a8230d2d5d9858511c8875b88db20c0642414245b5010367000000b28f9d110000000036f77094e2f2bf00d706357848c57740575e959c3345c648da72f07f9f598e0ebc6ee44a916f97c65f9d26b8d6b6608204755548f5ef8b528569a93cacc2540ebfe1d2614499a1dc8cd5356d8099d5fe34bc7e42a82d29779bec4ed8af75fb05044245454684038f927d020ef9925ccc51918e2b2543ab59fc5dcdce343de358a8245a9770652105424142450101543e56f9498eb078963b24d14dc24b440ae06eb79438e2ef284e94bb49228a61e64cd714992a61eadbbd2b06b0e45f6bc308a477d5d4ee5d099d0b3aaa81e28b","grandpaAuthoritySet":"0x6502ad4be70209b5c84f0d9a26d1b476570226c6237af41bd89566fdb809e99a66b80100000000000000384c6ec2c8511c9454f75b9f59a30a689cbb2ec34037027d2f3acecbeb8abe75010000000000000047646f5f2395d6e5856a2e5646feba7451e65f4efb097c1d080352d36a656c8001000000000000006b4f558832a86f088340ca1ebabe2147823036c6abbde36e4e0be8dcd5c18f2901000000000000003fa770bbcfbdaac57d9923d9d854e98d76c1d59f5ca976adca479e6850324b0801000000000000009b2df55c2234271d1c9d5cd4291301ec624ee6d13bddf78dd8fc87c31d42fc9a010000000000000084beadb089cda5e0bc20bbc08665499c4d641bb189864f58e40a71a12b1bd776010000000000000076ed6307d6470b4c239398b36937b4e3ca0fb8157daff442c33442eefe2814c801000000000000004aa5708f54845419489cb1aae67c20048d062dfa9ea0b9a84de18376f4ce92c60100000000000000557b8639195aafda6700108fb6df0cc47253f8cde2d2b6113bed282e277c614d0100000000000000823557a6b0d397bcd3d188aa06fc13e49c636cbdbf35eccafd2d23595e6589e20100000000000000c87458e9d5cce432da904052ad78d2d950ac6ae7a462a39f7396434b0d1b32ff0100000000000000381e3981cbe65010b643ea621caacfee6f4c5a30daaec93b45fced51e7ddb1bf0100000000000000f30a20b577ee6bea8a45b9cf9ad8417d0078f7786acf7b185260f4b651631ed10100000000000000d040d9cf3ae82b37c7ff336b00691e6871ecbf80e6a6c97d2d5901ad82e0b4e301000000000000008560ab563f2a3d9b84de611fcfc2feda7a6b19bf5e61cf20f9fc63bc138de4e20100000000000000af69664425d84ef811ffae3cbe6ac6fbe2063c18ac74a655c2cbd6c4339987720100000000000000d23c267273b83bb35c8e838a2b6c47c49af8f15bb03b4ebbc0c171c0242cb15c0100000000000000f5b183712646f29d40c2c0877305cb0d902c214f0a8d44c5b8e6c765384740b401000000000000002bf2597eda54f89e851d398be9c76ed7d8a43948ca60418c7361ec5bce6ab50e01000000000000002f6469ed6f2f9e92cb3126281c8bf6ff65e77f205681b37aa7b9c909b168e201010000000000000021214b3c44747d87d048d179c55dacba3c02e74ce77e0b845bef3444952cfac3010000000000000099ab99a1b0af27bd480542c88b35a6624ef2121de54329582811aa9b31a3c733010000000000000022d04ea680d28674d9809c2022b38e60911447657af9abe8eaedac409ae2deff0100000000000000c3af638f3ed2948314eb771e970f73eceac5a2e101ea327fa8fe43e9ee61dda80100000000000000719ea231a2dada4838ddfd19beb3ac7ce248b660b1fccda83b033d0865d2aa4701000000000000008a75b8423c11621120418655cb54f75f0eb73984942e0344bc47071c693be638010000000000000045b06ca79993b20a511dda12cbbccd2c43d1e1eb827ecf826620286045cf4fcb01000000000000006902cc7446affffa43ea696c8c0507a3c38ca0faf868544a7ab3d573019f3ca201000000000000001cdd699d681de6c6ef2405e25743a09b2d94548c840d0f1c9cdc5f4e0a856efb01000000000000006efdc9c518967ad4229eb48e549fec8263aec4a1910326235118e36335a6fc1701000000000000005eec271b03fcf4277015ab182472fc55719511240c8bef8545195312d351f7750100000000000000479f72aef93619c23bc5d81f1939c90ba1a80651d110720cc49dd50e8cb7b077010000000000000004bba58e0b53f515010d1cc56453c10c9333b8fa5ffc1412621dec556860b8e4010000000000000059750fcc03242d2eca42f5827074f93442ce3fe47f03847dd0dead3e29eb0e820100000000000000dbdb0b2b2e7b20de3f85dc61ab8fe9320c6d1e8c7323f60d08a909a31cf2ffed0100000000000000e4e71ed676259af3363516b55b604077015f662a84f3aabecf125fe8921b0ea70100000000000000eff1d5c906c90c1b482280619cabc0762d3dcb793cdbf6e26d5b00d14cda38c90100000000000000c80773fbc6144ba0c3d5b35a6e46f6ca2998513d172a8818bce2445f3365b6e901000000000000006a9e0c68b5f539f69cb01be910e5d5a299bb2a50b3b9b0a033a69416ed57b15b0100000000000000c6c9d24ef1f3bf0fa99aa5f0bafe680d8ef42e421398bcbe691f7bcb617bfa140100000000000000400ba8e74fc9470b03de528e828b8741e9455f4718eccfc2ea04499bd321f62901000000000000004b38d9dad38539affc9f0fd20471e6a3f55752fc9d7cd0dc035a681c2eab81f501000000000000005e40741a21cdd46e7071174f3801f07b492b0941540454835383f46396849b3e01000000000000008b4f995dbf0667e2c14342cbd141e3a87bd1c3dac56c4faaf3736a1e826ba69601000000000000007e939b0b53123a2e357da8247e659430c310feeb89836d2224e0a65bda419c2d0100000000000000d84ecc11b2fcc511e3799db37695cd9f1bea7e581f7f259c5f70ad6e41d74b0d01000000000000004d5e8f0e248bf0ebdf8c708b1d61e475162bcedded321222528402f10811eabd01000000000000007251da7147c58d25365bc6245897d11a6656be57fa3f9ad095746d3d2556e86201000000000000003d105567dd34373f70e746243abfd00b0eede0e36f2836ec7a8ac95c26acc9fa01000000000000003bc2c8a405f7acd25a5e6c1cee72ce8e2c247b05c6b37b9c2bf7654a624e0b4601000000000000004fbba25109fe9ee6e27322db51f40c56ad8940fa9388de42ca43bcc9f2c221a0010000000000000036b8caeb2b1963addacda511e61f005e298bb7aa2a6c67a25c0d8c49bf46640901000000000000007919f465947953a6d82378e215561a6214d4b14b7bf519c2204cdc2079a2226c0100000000000000ea15fedde78eebcf26a7646dc5489518bb1c3d21598591483cded88fab68c0470100000000000000bb621691b37e0a1e5006f09c9c43e7ba19a43edeaa383654c8576efdaa01245401000000000000003a960631477eaa37fbeea5016d0887240207e86a05537d39e61d6250e2238d340100000000000000e0d591aa1c2d5fe4ca6372e3f305c529095dac8beaf140296f9ccf4696223f5f010000000000000088554c14ccee6a400aeed70df6b44541e301db7546b72071439658cbc8b6d31c01000000000000009f263ae8823a331e0dfd297da617def759acc03cf409767cbd8edcc79f4f3f580100000000000000f09a897aa6df3fc451bbfa12d072b6805145adbcc50ced15f6227a9d5d145b17010000000000000046fcf86f16a521650a94722efd176b0c9467fb48cbe3e9c40fae86ea27322e110100000000000000c9ee90f0b392fd960e207febc29146a8e045a955c8c06588d9d2e1080751b59e0100000000000000be73d3cad71c400291d1f1ef1d3f25b5924f5efa2d509e62389106d5d28eab8b010000000000000078e0a781a72114b2ea5689cadf5401c8122b04f799985c3ea3c41c936981cdc40100000000000000b86e7a22d2cc7ff75e1a204b5f0b6d8a660e067e100755da2375e73d28ade4060100000000000000e2f4ca20d0305eb14715e8fa015b7125487490d3b2ddda90e4a3b3ace8b44a68010000000000000006081b1806b53c3dbc7a2fc3c5932f3dd3dbcde6e3cc865a39913f5e6352c4b90100000000000000a23c14809e06b77499b621c7528676377f2561a8ca43f5ec2ab86aba1929445101000000000000003468a2de1b18afd7978a2dedeef06d46651ab473181d0e063374d12e026dcf2c010000000000000060d1fe50e6f42402921500bcc8ef367e160c51d2f63fe6b1e81c703910320c1d010000000000000056db972358d8170db0515ad31c26a0e9706c566d9180bc7034863bc81accf9290100000000000000c9da738b1fe72e00496e884864a0b9d521ea981cab204291809de08462a263cb0100000000000000fa93ba85b41840cfdb6e7fc74ae3a01210485205532688f1b906e2d7abe671340100000000000000ece9eb501e7c78a616f8513338d51ad757996feefdfaae112e22941eddcd054e01000000000000008c9ddc6eeccbe0c730d98d6893841dbf51aa8ff8e983854c0ad3c8d090b368df0100000000000000bde44832d1d09b2dcaa74de928b9776f407dfdadbd52d28186cb5915d5d43c1101000000000000002c87d973cebba378cf91c1cb2b95ec4dc8bf8971daa6fb53528e31f46fcad6fa010000000000000048504abf30e2e4edeb0fa806f5c68a343c5351ba3b2c1221900af24a22b87b860100000000000000485d0afb31b273587c30ef16e9b2d085fd4d54f5735f50e678a71915f69717c70100000000000000705203fab555c0993842e562824ae52bee09dac6d79a47391588263eb2119b7d01000000000000006b65130f5d57f462fd3bb571406ea82f3851e95803e612d798aae10fbfb679dd01000000000000006cd9a2a780fd1600a86a28687562f57d01f7351ca4eef324a47cd694cb2c12ab010000000000000018c2337721ae8ea495e78e0de50c80810f5164ba9f37fbab7db8046c5a43c16e0100000000000000730b398581b2627e17fc1859f2b25a01d00cd10a0fad10bda7b5ece5a677cef9010000000000000070a8f2c854fa3a17a68746cfe493beb9bff8f34bbcd83d4a1dac92a82a6439260100000000000000da407968218979b6cf9f3b273d5da9d6fed41de48f203d94d85d73bc3c3775350100000000000000b6330a5333954bf601044454fb9bec3401ed59daad584bf62dd6141bdbe33bc30100000000000000727d484a95bb237b70c66aad2fdc6906595ae24784d4a4cbaa048e747cb5cfd001000000000000007a9c41976043382288ecade1687ff78cdbf892bae9f4886606fc0ed59add6c2c0100000000000000af732cc90dd5fff6d8f554af6a5eb13c0b300f4232ff516ab849f19c88d74ba50100000000000000a6eeea7cd93e70197d91d68e62c151023ac3eef1beb38af9af359634585df6c501000000000000009cb6f96ecc63671d4d40544cf69cbde1bf2f5d679ae1fcece0a256c7c9e8f1710100000000000000da550603ca4b3442d7d6ef7526b259c9290a317d2495d6cbb476279f18a42836010000000000000050cfc7356353c0fa42aa8b43ac5d2ec0f6eeb5095ef9a5a6abc0c8b8ec2963c001000000000000007eef4e31aaa7f605a0e924e1ffdaee0c1dfcc0685e7a7c5739006ba602def8cb01000000000000006fb539e77dc8f11e27f51ffbbe29a22f9c65886cc350a180cc15df1fd33739740100000000000000b18cbeac96b0399f881de179ce8dff877a85a9c88844c5ec4cff5b1ae0c65cb30100000000000000eeb7717a1ee2ebd5854f3426d8f276651693403000fa939f4073e4474b75e0100100000000000000a753a68985d94a0771b704364571c810cffab16a8fd450b4d79744b0fe89f4e90100000000000000f1e80cc588d39ad6464d5e890a704d5e9fc07920903b08f06b4b3a2ad218595e01000000000000009b0dd401864ea25913c2d5987428a3aaf33c100465843198d72e37048febcaec0100000000000000991b8f0c7c42ef75dfb24d1df053dbe18c4b2bafdd86803d2b2505a688bf7f71010000000000000019b84719ba987dd028e1fcf512a0ee857a1b53285161f586b456547f86f7fc7d0100000000000000382b9ac9a4895a6508b935b3014345a637745bd58b3335d54a1e14215b5d178e0100000000000000c0732b159411a4abdb0c29029c1ec2013fead26e03fe87b5f5ec4366435db85b010000000000000006fc73229bea563d9d98f48b4ef575dbb5570ef338798255fbb3da68bc4fff6401000000000000004a56d18eebb59c1d0480a8d0d60117693888aab705c481d4c22545e9312749b30100000000000000bddf5cbdced272ebcfa5187c44ebbc22a614a840ca015a50f489ec373866d5f30100000000000000035410457a842370fc973b116be0ceabfa81f8e1b3bb162922787bfc95f1a39d0100000000000000c8af78597e7250805b1f906518e1573e92f1740ddf5afea5a655625f8de7e39c01000000000000001eda33b98265b1141eb06d7079c7cb51419dc2170dbc998cb9628fe26b340ce801000000000000009cc46f39c94d6dde4865a254f9c8eb3138c4b3576ffbf7e62f790b531038ef7801000000000000001718f4699bd5ca24b0a0951f2e8667d5344230f86ddfe4778bf4852921b368710100000000000000ea316f1bface500d1f76d20400396f2d3ef4a7b18a4353133cf821b9a1148a560100000000000000c3c72e825de631fd2deb715a2170a70580431b11666639d6d09f215fd587cb7701000000000000007d73f6b78728b31063d40af90af3e6d224d71d9ab362f32da11ea255337963a9010000000000000075e1574177fdbb8863b5b247b2d3136033df4d10f9a775ce3af2123fc97dbfed010000000000000099506d8a67e458bff8ceb93cb4d77f23957138399f11fc44996e61b1df1848ea010000000000000078b5370952428e4882c764e2e14a8217710c61b03244c5dd10c9a2f3f5c05c250100000000000000d23988121a61813005168196899b929406d4f9ee0a523f3759e6e477b89261cd01000000000000004beaea3024e4c9c573439e9a087a6227c46c5bf052b7a28c296f05e7b3d8d72e0100000000000000e1cd12e7985adf9a9a5328972f8f66d035fe9ed9a50d6e01a5970146eb4cd6c20100000000000000bf47ea951bf20303e3ace58a8e95b79e1430c86b7ce97cc14b09f643e57192b201000000000000003fdbfac7e80374c510aaad223d447a49b76196c9a60e88d726d2c91760fffaaa0100000000000000cd9adcc8236338070016de846f0026051e7ae07f91f7cf39574a7262cd230728010000000000000038cff2dd11deb5999ed2055d98fd436b4858a01a43287f286dd6c932102638cd0100000000000000e87af73a4a8195b80e2c977edca55275622a0fbca48e6dd93b7c0db3ed54d427010000000000000014fa616d5b1d2f5403eee710709323ce2473f08d77b9e00d20ed517e52f4d9bc01000000000000005873f921ecc605ae9f4a98a5b59505a652fd643bb47ff66ab7cc096e957dea400100000000000000814193cc0520ea91284ed44e13979044c91771c9f508255bdd33f1c5d19183dc0100000000000000c77400bbd98289422195bf6bf8b0b57911002f77dcfe82d7e4e74f38a5e409b601000000000000000ee3e9057a71d6c6501548b6e3785802de52b8a5e87d9b9e521eea2b05918b2f0100000000000000d2096c995e2bfdf52a7c60fe4db0d64bb64d9fc9e35df3bec5e7c6c837fe45a3010000000000000064fa44e74901bd0498d7f005e757c8269cbad75323d6c678fad732205c184e5c0100000000000000cec685cdd056218ba7e245c98fdc103ae6c4606164336c51ce8ea9092f7972bd0100000000000000235b304ea70c53f060701ecb4257e03d7c1bdac282d940865534533f272c95ec010000000000000015dac847cdd7eb29e0ca6dc9e9586c5cfc6cda407692459413dd41de69589dc40100000000000000db257ed1026021a81ef5b1848dfe8588439156e20a177a9ce778973f3548d2430100000000000000d4fe9c70a8c767339e6c8f7b29ff32ed8f89c3655ee5e27df217ff2fb0f2304e010000000000000050c1a5d1e221782ddbdcaf039dc23d39691b773a9ca543305f0809ce5b31fefc0100000000000000e5b4449610d8c33729d096216500ad634fe3654420f70b7fbaeb746ff8ba973a01000000000000000eb6a7b2b5758eef2088dc65b067d281ead3252f09062d0909f5b4bdec6be88e0100000000000000f5bf306dee98b6057c9cd931f5f1c9c9d36eb591ace063ec8bb3b1150a31f2e5010000000000000081b637606f92c6d01c8c7e11e6face0a289f008f568b42c3a47d2111a95f57c301000000000000001c14c0ce66bf74a93290ed75209d56dc729890c70fba1ec6c23ff9f12daf4de101000000000000002fb5594b7750ee0cafdde385912ea201ae53162aa66ee5588c0360391773f88801000000000000001339c2c7ee10e5ca4119113db8c140c250e41e93b22927c54bc5075318c6c1e80100000000000000f2881bc07407ab75cca9e3c1a93174b8a97f69362d39d35c755a1b426f131434010000000000000015a4d7b82bacbdbcec37aa687061702d65fd490d75562dda60db8da46961231401000000000000009c07a4b0db05b434a94e31377d254bc3c797b79212eb7e447b4f12a7c1adb12c010000000000000051d8caf8c6a388dc49cae5fe9be4f04dd889893057a011810aa0a1149a0debf10100000000000000cb4e9c0fd534185b7c365e954d670bb1fdf6fcd70e965c4caca59e262ded55b20100000000000000e90b0000000000000001cdcfa30000a52f00000000000000005909000001000000000000008b15000002000000000000000f2300000300000000000000c72e000004000000000000001f31000005000000000000002f3f000006000000000000003f4d000007000000000000004f5b000008000000000000005f69000009000000000000006f7700000a000000000000007f8500000b000000000000008f9300000c000000000000009fa100000d00000000000000afaf00000e00000000000000b9bd00000f00000000000000c9cb00001000000000000000bfd900001100000000000000cfe700001200000000000000dff500001300000000000000e90301001400000000000000f91101001500000000000000092001001600000000000000192e01001700000000000000273c01001800000000000000374a01001900000000000000475801001a00000000000000576601001b00000000000000677401001c00000000000000778201001d00000000000000869001001e000000000000003a9e01001f0000000000000049ac0100200000000000000057ba0100210000000000000067c80100220000000000000077d60100230000000000000085e40100240000000000000095f201002500000000000000a50002002600000000000000910e02002700000000000000471c02002800000000000000452a02002900000000000000393802002a00000000000000184602002b00000000000000255402002c000000000000000c6202002d00000000000000c56f02002e000000000000003e7d02002f000000000000000d8b02003000000000000000ed9802003100000000000000d8a602003200000000000000cfb40200330000000000000083c2020034000000000000005fd00200350000000000000058de0200360000000000000057ec0200370000000000000067fa020038000000000000006a0803003900000000000000f81503003a000000000000009e2303003b00000000000000823103003c00000000000000903f03003d00000000000000a04d03003e00000000000000b05b03003f00000000000000be6903004000000000000000cd7703004100000000000000dd8503004200000000000000ed9303004300000000000000fda10300440000000000000055a403004500000000000000fcaf0300460000000000000005be030047000000000000000bcc030048000000000000000eda0300490000000000000015e803004a0000000000000025f603004b00000000000000330404004c00000000000000421204004d00000000000000512004004e00000000000000612e04004f00000000000000713c040050000000000000007d4a040051000000000000008c58040052000000000000009c6604005300000000000000ac7404005400000000000000b78204005500000000000000c69004005600000000000000d69e04005700000000000000e6ac04005800000000000000f6ba0400590000000000000006c904005a0000000000000016d704005b0000000000000024e504005c0000000000000034f304005d00000000000000420105005e00000000000000520f05005f00000000000000301d05006000000000000000312b050061000000000000004139050062000000000000005047050063000000000000006055050064000000000000006f63050065000000000000007e71050066000000000000008d7f050067000000000000009d8d05006800000000000000ab9b05006900000000000000bba905006a00000000000000cbb705006b00000000000000dbc505006c00000000000000e6d305006d00000000000000f6e105006e0000000000000006f005006f0000000000000016fe05007000000000000000260c06007100000000000000361a060072000000000000003028060073000000000000003e36060074000000000000004d44060075000000000000005c5206007600000000000000486006007700000000000000566e060078000000000000005e7506007900000000000000657c06007a00000000000000738a06007b00000000000000839806007c0000000000000092a606007d00000000000000a2b406007e00000000000000b2c206007f00000000000000c2d006008000000000000000d2de0600810000000000000032e806008200000000000000e2ec06008300000000000000f2fa06008400000000000000020907008500000000000000121707008600000000000000212507008700000000000000313307008800000000000000893507008900000000000000414107008a00000000000000514f07008b00000000000000615d07008c00000000000000716b07008d000000000000007f7907008e000000000000008f8707008f000000000000009f9507009000000000000000afa307009100000000000000bfb107009200000000000000cfbf07009300000000000000dfcd07009400000000000000efdb07009500000000000000ffe9070096000000000000000ff8070097000000000000001f06080098000000000000002f14080099000000000000003f2208009a000000000000004f3008009b000000000000005f3e08009c000000000000004a4c08009d00000000000000315a08009e00000000000000da6708009f00000000000000b1750800a00000000000000022830800a100000000000000c9900800a2000000000000005b9e0800a300000000000000eeab0800a4000000000000008bb90800a50000000000000016c70800a600000000000000acd40800a70000000000000035e20800a800000000000000ccef0800a90000000000000059fd0800aa00000000000000ea0a0900ab0000000000000078180900ac000000000000000b260900ad00000000000000a0330900ae000000000000002f410900af00000000000000be4e0900b000000000000000535c0900b100000000000000df690900b20000000000000064770900b300000000000000e8840900b40000000000000073920900b500000000000000fd9f0900b6000000000000009cad0900b70000000000000037bb0900b800000000000000cbc80900b90000000000000062d60900ba00000000000000e8e30900bb000000000000007df10900bc00000000000000edfe0900bd00000000000000870c0a00be00000000000000141a0a00bf00000000000000ac270a00c0000000000000003d350a00c100000000000000c4420a00c20000000000000050500a00c300000000000000de5d0a00c4000000000000007f6b0a00c50000000000000016790a00c600000000000000a3860a00c7000000000000002b940a00c800000000000000faa10a00c9000000000000008eaf0a00ca0000000000000061bd0a00cb0000000000000041cb0a00cc0000000000000017d90a00cd00000000000000e7e60a00ce00000000000000b9f40a00cf000000000000008a020b00d00000000000000098100b00d100000000000000a81e0b00d200000000000000b82c0b00d300000000000000be3a0b00d400000000000000cb480b00d500000000000000db560b00d600000000000000eb640b00d700000000000000fb720b00d8000000000000000b810b00d9000000000000001b8f0b00da000000000000002b9d0b00db0000000000000039ab0b00dc0000000000000049b90b00dd0000000000000059c70b00de0000000000000069d50b00df0000000000000079e30b00e00000000000000089f10b00e10000000000000099ff0b00e200000000000000a90d0c00e30000000000000059120c00e400000000000000b1140c00e50000000000000009170c00e600000000000000b91b0c00e700000000000000111e0c00e80000000000000019250c00e900000000000000c9290c00ea0000000000000081350c00eb00000000000000d9370c00ec00000000000000e9450c00ed00000000000000994a0c00ee00000000000000f8530c00ef0000000000000050560c00f00000000000000008620c00f10000000000000018700c00f200000000000000287e0c00f300000000000000388c0c00f400000000000000439a0c00f50000000000000052a80c00f60000000000000060b60c00f700000000000000acb80c00f800000000000000b3bf0c00f90000000000000063c40c00fa00000000000000c3cd0c00fb0000000000000073d20c00fc0000000000000073e00c00fd000000000000004dee0c00fe000000000000005bfc0c00ff00000000000000580a0d000001000000000000b00c0d00010100000000000068180d00020100000000000076260d00030100000000000084340d00040100000000000094420d000501000000000000a4500d000601000000000000b35e0d000701000000000000c36c0d000801000000000000d37a0d000901000000000000e3880d000a01000000000000f3960d000b0100000000000003a50d000c0100000000000013b30d000d0100000000000023c10d000e0100000000000032cf0d000f0100000000000041dd0d00100100000000000051eb0d00110100000000000061f90d00120100000000000070070e0013010000000000007e150e001401000000000000de1e0e0015010000000000008e230e0016010000000000009e310e001701000000000000ac3f0e001801000000000000bc4d0e001901000000000000cc5b0e001a01000000000000dc690e001b01000000000000e9770e001c01000000000000f1850e001d0100000000000001940e001e010000000000000fa20e001f0100000000000012b00e00200100000000000022be0e00210100000000000032cc0e00220100000000000042da0e00230100000000000052e80e00240100000000000062f60e00250100000000000072040f00260100000000000081120f00270100000000000091200f002801000000000000a12e0f002901000000000000843c0f002a01000000000000944a0f002b01000000000000ec4c0f002c01000000000000a1580f002d01000000000000b1660f002e01000000000000b5740f002f01000000000000c5820f003001000000000000d5900f003101000000000000e59e0f003201000000000000f5ac0f003301000000000000e2ba0f003401000000000000b8c80f003501000000000000c8d60f003601000000000000d7e40f003701000000000000dfeb0f003801000000000000e7f20f003901000000000000f70010003a01000000000000f80710003b01000000000000000f10003c01000000000000101d10003d01000000000000202b10003e01000000000000303910003f010000000000004047100040010000000000005055100041010000000000006063100042010000000000005d7110004301000000000000607f10004401000000000000708d10004501000000000000809b1000460100000000000090a9100047010000000000009eb710004801000000000000aec510004901000000000000bed310004a01000000000000c9e110004b01000000000000a1ef10004c0100000000000093fd10004d01000000000000760b11004e01000000000000811911004f010000000000006d27110050010000000000007d3511005101000000000000894311005201000000000000995111005301000000000000865f11005401000000000000636d11005501000000000000537b1100560100000000000063891100570100000000000072971100580100000000000082a51100590100000000000092b311005a01000000000000a2c111005b01000000000000b2cf11005c01000000000000c2dd11005d01000000000000d2eb11005e01000000000000e2f911005f01000000000000f207120060010000000000000216120061010000000000000b24120062010000000000001b32120063010000000000002b40120064010000000000003b4e120065010000000000004b5c120066010000000000005b6a120067010000000000006b78120068010000000000007a86120069010000000000008a9412006a010000000000009aa212006b01000000000000aab012006c01000000000000babe12006d01000000000000c9cc12006e01000000000000d9da12006f01000000000000e9e812007001000000000000f9f612007101000000000000090513007201000000000000191313007301000000000000282113007401000000000000382f13007501000000000000483d13007601000000000000584b13007701000000000000685913007801000000000000786713007901000000000000887513007a01000000000000988313007b01000000000000a89113007c01000000000000b69f13007d01000000000000c6ad13007e01000000000000d5bb13007f01000000000000e5c913008001000000000000f5d71300810100000000000005e61300820100000000000015f413008301000000000000230214008401000000000000331014008501000000000000291e14008601000000000000372c14008701000000000000473a14008801000000000000554814008901000000000000655614008a01000000000000756414008b010000000000007c6b14008c01000000000000c56d14008d010000000000001d7014008e01000000000000757214008f01000000000000858014009001000000000000958e14009101000000000000a59c14009201000000000000b4aa14009301000000000000c0b814009401000000000000d0c614009501000000000000e0d414009601000000000000f0e21400970100000000000000f11400980100000000000010ff14009901000000000000200d15009a010000000000002f1b15009b010000000000003f2915009c010000000000004f3715009d010000000000005f4515009e01000000000000175115009f010000000000006f531500a0010000000000007f611500a1010000000000008f6f1500a2010000000000009f7d1500a301000000000000ae8b1500a401000000000000b8991500a501000000000000c8a71500a601000000000000d8b51500a701000000000000e8c31500a801000000000000f8d11500a90100000000000008e01500aa01000000000000c0eb1500ab0100000000000018ee1500ac01000000000000eefb1500ad01000000000000b9091600ae010000000000007c171600af0100000000000043251600b00100000000000021331600b101000000000000583c1600b201000000000000f3401600b301000000000000bb4e1600b4010000000000007f5c1600b501000000000000496a1600b6010000000000001d781600b701000000000000e3851600b801000000000000ba931600b9010000000000009aa11600ba010000000000006daf1600bb010000000000004abd1600bc0100000000000021cb1600bd01000000000000f3d81600be01000000000000b9e61600bf010000000000008cf41600c001000000000000c8fd1600c10100000000000031021700c2010000000000006c0f1700c301000000000000bc1c1700c401000000000000fe291700c50100000000000051371700c601000000000000a7441700c701000000000000e7511700c8010000000000004d5f1700c901000000000000ba6c1700ca01000000000000867a1700cb0100000000000084881700cc0100000000000073961700cd0100000000000043a41700ce0100000000000048b21700cf0100000000000058c01700d00100000000000068ce1700d10100000000000078dc1700d20100000000000088ea1700d30100000000000098f81700d401000000000000a8061800d501000000000000b8141800d601000000000000c8221800d701000000000000d8301800d801000000000000e83e1800d901000000000000f84c1800da01000000000000085b1800db0100000000000018691800dc0100000000000028771800dd0100000000000038851800de0100000000000048931800df0100000000000058a11800e00100000000000068af1800e10100000000000078bd1800e20100000000000088cb1800e30100000000000097d91800e4010000000000008be71800e50100000000000056f51800e60100000000000034031900e7010000000000001c111900e801000000000000141f1900e901000000000000242d1900ea01000000000000333b1900eb0100000000000042491900ec0100000000000052571900ed0100000000000061651900ee010000000000006b731900ef0100000000000074811900f001000000000000198f1900f101000000000000bb9c1900f20100000000000066aa1900f30100000000000011b81900f401000000000000a7c51900f5010000000000004dd31900f6010000000000008bde1900f701000000000000c9e01900f801000000000000cde91900f9010000000000004dee1900fa01000000000000e2fb1900fb0100000000000070091a00fc01000000000000fb161a00fd0100000000000093241a00fe0100000000000028321a00ff01000000000000b63f1a000002000000000000414d1a000102000000000000d35a1a0002020000000000005b681a000302000000000000e9751a00040200000000000078831a000502000000000000e6901a000602000000000000639e1a000702000000000000efab1a0008020000000000007bb91a0009020000000000000cc71a000a02000000000000a9d41a000b0200000000000029e21a000c0200000000000098ee1a000d020000000000009efa1a000e02000000000000f1061b000f020000000000004d131b001002000000000000771f1b001102000000000000a72b1b001202000000000000ce371b001302000000000000f0431b00140200000000000026501b001502000000000000415c1b0016020000000000008e681b001702000000000000bd741b001802000000000000e9801b001902000000000000488d1b001a02000000000000a3991b001b020000000000000fa61b001c020000000000004fb21b001d0200000000000060bf1b001e0200000000000029cd1b001f02000000000000f4da1b002002000000000000bce81b00210200000000000057f61b002202000000000000dd031c002302000000000000320f1c00240200000000000074111c002502000000000000291f1c002602000000000000cf2c1c0027020000000000007e3a1c00280200000000000027481c002902000000000000db551c002a0200000000000091631c002b020000000000003a711c002c02000000000000fa7e1c002d02000000000000a58c1c002e020000000000005d9a1c002f020000000000000da81c003002000000000000d7b51c00310200000000000087c31c00320200000000000030d11c003302000000000000ddde1c00340200000000000088ec1c00350200000000000033fa1c003602000000000000cbfe1c003702000000000000fd071d003802000000000000c8151d0039020000000000009c231d003a0200000000000053311d003b02000000000000233f1d003c02000000000000eb4c1d003d02000000000000bc5a1d003e0200000000000095681d003f0200000000000069761d0040020000000000003f841d00410200000000000016921d004202000000000000c89f1d0043020000000000008ead1d00440200000000000046bb1d0045020000000000001bc91d004602000000000000e3d61d004702000000000000c1e41d00480200000000000095f21d00490200000000000069001e004a020000000000003a0e1e004b02000000000000d3191e004c02000000000000251c1e004d02000000000000032a1e004e02000000000000cf371e004f02000000000000b53e1e0050020000000000009b451e00510200000000000078531e0052020000000000004c611e0053020000000000000b6f1e005402000000000000d37c1e005502000000000000488a1e00560200000000000012981e005702000000000000eda51e005802000000000000bdb31e0059020000000000008ac11e005a0200000000000067cf1e005b020000000000001fdd1e005c02000000000000ceea1e005d0200000000000088f81e005e0200000000000034061f005f02000000000000e0131f00600200000000000091211f006102000000000000532f1f006202000000000000fb3c1f006302000000000000bd4a1f00640200000000000066581f00650200000000000016661f006602000000000000bf731f00670200000000000070811f006802000000000000268f1f006902000000000000ca9c1f006a0200000000000071aa1f006b0200000000000023b81f006c02000000000000e7c51f006d020000000000007cd31f006e020000000000003ae11f006f02000000000000e1ee1f0070020000000000007dfc1f007102000000000000c5fe1f0072020000000000000e0120007302000000000000300a20007402000000000000e817200075020000000000007d2520007602000000000000053320007702000000000000944020007802000000000000294e20007902000000000000b45b20007a02000000000000386920007b02000000000000be7620007c020000000000004e8420007d02000000000000138b20007e02000000000000e59120007f020000000000007a9f200080020000000000002dad20008102000000000000d3ba2000820200000000000058c820008302000000000000f4d5200084020000000000008be32000850200000000000028f120008602000000000000c5fe200087020000000000005b0c21008802000000000000e41921008902000000000000271c21008a020000000000007c2721008b020000000000001c3521008c02000000000000b24221008d020000000000002c5021008e02000000000000c85d21008f020000000000006b6b21009002000000000000047921009102000000000000a98621009202000000000000469421009302000000000000d8a1210094020000000000007eaf210095020000000000001ebd21009602000000000000b0ca2100970200000000000060d821009802000000000000fae52100990200000000000091f321009a02000000000000210122009b02000000000000a20e22009c020000000000002e1c22009d02000000000000c02022009e02000000000000c82922009f0200000000000059372200a002000000000000d8442200a10200000000000074522200a2020000000000000c602200a302000000000000906d2200a402000000000000267b2200a502000000000000be882200a6020000000000004c962200a702000000000000d2a32200a8020000000000005eb12200a902000000000000e5be2200aa02000000000000a8c52200ab020000000000008dcc2200ac0200000000000067da2200ad02000000000000ebe52200ae0200000000000037e82200af0200000000000010f62200b002000000000000e3032300b1020000000000009f112300b202000000000000531f2300b3020000000000002c262300b4020000000000000f2d2300b50200000000000073382300b602000000000000bc3a2300b702000000000000053d2300b8020000000000009f482300b90200000000000093562300ba0200000000000082642300bb020000000000006f722300bc020000000000005f802300bd020000000000006d8e2300be02000000000000c5972300bf02000000000000739c2300c00200000000000081aa2300c10200000000000091b82300c2020000000000009fc62300c302000000000000afd42300c402000000000000bee22300c502000000000000ccf02300c602000000000000dafe2300c702000000000000e90c2400c802000000000000f51a2400c90200000000000005292400ca0200000000000013372400cb020000000000001b452400cc0200000000000026532400cd0200000000000034612400ce020000000000003e6f2400cf020000000000004e7d2400d0020000000000005d8b2400d1020000000000006d992400d2020000000000007ba72400d30200000000000088b52400d40200000000000096c32400d502000000000000a4d12400d602000000000000b3df2400d702000000000000c3ed2400d802000000000000bafb2400d902000000000000ad092500da02000000000000b0172500db02000000000000ae252500dc0200000000000052312500dd02000000000000aa332500de02000000000000ba412500df02000000000000c84f2500e002000000000000d55d2500e102000000000000e26b2500e202000000000000ec792500e302000000000000f4872500e40200000000000004962500e50200000000000014a42500e60200000000000021b22500e7020000000000002ec02500e8020000000000003dce2500e9020000000000004ddc2500ea0200000000000058ea2500eb0200000000000067f82500ec0200000000000077062600ed0200000000000085142600ee0200000000000094222600ef02000000000000a1302600f002000000000000ad3e2600f102000000000000b84c2600f202000000000000c55a2600f302000000000000d3682600f402000000000000c6762600f502000000000000ca842600f602000000000000d8922600f702000000000000e8a02600f802000000000000f4ae2600f90200000000000004bd2600fa0200000000000013cb2600fb0200000000000023d92600fc0200000000000033e72600fd0200000000000042f52600fe0200000000000052032700ff02000000000000621127000003000000000000711f27000103000000000000802d270002030000000000008f3b270003030000000000009549270004030000000000003e4e270005030000000000009e5727000603000000000000aa6527000703000000000000b97327000803000000000000c98127000903000000000000d68f27000a03000000000000e59d27000b03000000000000f2ab27000c0300000000000001ba27000d030000000000000dc827000e030000000000001ad627000f0300000000000027e42700100300000000000034f227001103000000000000340028001203000000000000420e28001303000000000000521c28001403000000000000602a280015030000000000006e38280016030000000000007d4628001703000000000000895428001803000000000000976228001903000000000000a47028001a03000000000000b47e28001b03000000000000b08c28001c03000000000000b59a28001d03000000000000c2a828001e03000000000000d1b628001f03000000000000e0c428002003000000000000eed228002103000000000000fde0280022030000000000000bef280023030000000000001bfd280024030000000000002b0b290025030000000000003b1929002603000000000000482729002703000000000000583529002803000000000000674329002903000000000000745129002a03000000000000845f29002b03000000000000946d29002c03000000000000a17b29002d03000000000000b18929002e03000000000000bd9729002f03000000000000cca529003003000000000000dbb329003103000000000000ebc129003203000000000000f6cf2900330300000000000004de2900340300000000000012ec2900350300000000000020fa2900360300000000000030082a0037030000000000003e162a0038030000000000004e242a0039030000000000005c322a003a0300000000000068402a003b03000000000000764e2a003c03000000000000845c2a003d03000000000000946a2a003e030000000000009e782a003f0300000000000055842a004003000000000000ad862a004103000000000000bb942a004203000000000000c4a22a004303000000000000d3b02a004403000000000000e3be2a004503000000000000eecc2a004603000000000000feda2a0047030000000000000ce92a0048030000000000006bf22a0049030000000000001bf72a004a0300000000000029052b004b030000000000007f072b004c0300000000000035132b004d0300000000000044212b004e03000000000000542f2b004f03000000000000633d2b005003000000000000714b2b00510300000000000081592b00520300000000000091672b0053030000000000009e752b0054030000000000009f832b005503000000000000a6912b005603000000000000ab9f2b005703000000000000b8ad2b005803000000000000c8bb2b005903000000000000d6c92b005a03000000000000e6d72b005b03000000000000f6e52b005c0300000000000005f42b005d0300000000000013022c005e0300000000000020102c005f03000000000000271e2c0060030000000000002d2c2c0061030000000000003d3a2c0062030000000000003b482c00630300000000000046562c00640300000000000055642c00650300000000000063722c00660300000000000070802c0067030000000000007f8e2c0068030000000000008e9c2c0069030000000000009baa2c006a030000000000009eb82c006b03000000000000aec62c006c03000000000000bcd42c006d03000000000000cae22c006e03000000000000d7f02c006f03000000000000e5fe2c007003000000000000f40c2d007103000000000000021b2d007203000000000000f8282d007303000000000000fe362d0074030000000000000c452d0075030000000000001b532d0076030000000000002a612d007703000000000000366f2d0078030000000000001a7d2d0079030000000000001f8b2d007a0300000000000018992d007b0300000000000024a72d007c030000000000002db52d007d03000000000000d6b92d007e0300000000000035c32d007f0300000000000042d12d00800300000000000052df2d00810300000000000061ed2d00820300000000000070fb2d0083030000000000007f092e0084030000000000008b172e0085030000000000009a252e008603000000000000a9332e008703000000000000b9412e008803000000000000c54f2e008903000000000000ce5d2e008a03000000000000db6b2e008b030000000000005a792e008c03000000000000f9862e008d03000000000000d6942e008e03000000000000e2a22e008f03000000000000dab02e009003000000000000ddbe2e009103000000000000e4cc2e009203000000000000f4da2e00930300000000000001e92e0094030000000000000cf72e0095030000000000001c052f0096030000000000002a132f00970300000000000036212f009803000000000000962a2f009903000000000000462f2f009a03000000000000533d2f009b03000000000000624b2f009c0300000000000070592f009d0300000000000078672f009e0300000000000084752f009f030000000000008e832f00a0030000000000009d912f00a103000000000000979f2f00a20300000000000099ad2f00a303000000000000a7bb2f00a403000000000000b4c92f00a503000000000000bfd72f00a603000000000000aae52f00a703000000000000baf32f00a803000000000000c9013000a903000000000000d80f3000aa03000000000000e51d3000ab03000000000000e72b3000ac03000000000000f6393000ad0300000000000005483000ae0300000000000015563000af0300000000000025643000b0030000000000002d6b3000b10300000000000034723000b20300000000000042803000b303000000000000508e3000b4030000000000005f9c3000b5030000000000006caa3000b6030000000000007ab83000b7030000000000007bc63000b8030000000000008bd43000b90300000000000099e23000ba03000000000000a8f03000bb03000000000000b5fe3000bc03000000000000b70c3100bd03000000000000a51a3100be030000000000009a283100bf0300000000000093363100c003000000000000a2443100c103000000000000b1523100c203000000000000bd603100c303000000000000cd6e3100c403000000000000dd7c3100c503000000000000ed8a3100c603000000000000fd983100c7030000000000000ca73100c8030000000000001bb53100c90300000000000029c33100ca0300000000000039d13100cb0300000000000047df3100cc0300000000000057ed3100cd0300000000000067fb3100ce0300000000000077093200cf0300000000000087173200d00300000000000096253200d103000000000000a6333200d203000000000000b5413200d303000000000000c54f3200d403000000000000d35d3200d503000000000000c96b3200d603000000000000bf793200d703000000000000b9873200d803000000000000b0953200d903000000000000aba33200da03000000000000bab13200db03000000000000c5bf3200dc03000000000000c5cd3200dd03000000000000d5db3200de03000000000000e5e93200df03000000000000f5f73200e00300000000000005063300e10300000000000014143300e20300000000000021223300e3030000000000002f303300e4030000000000003b3e3300e5030000000000004a4c3300e6030000000000005a5a3300e70300000000000068683300e80300000000000077763300e90300000000000087843300ea0300000000000096923300eb03000000000000a0a03300ec03000000000000afae3300ed03000000000000babc3300ee03000000000000caca3300ef03000000000000d9d83300f003000000000000e4e63300f103000000000000f1f43300f203000000000000f8023400f30300000000000007113400f403000000000000161f3400f503000000000000262d3400f603000000000000303b3400f7030000000000003f493400f8030000000000004c573400f90300000000000058653400fa0300000000000064733400fb030000000000006b813400fc03000000000000798f3400fd03000000000000889d3400fe0300000000000098ab3400ff03000000000000a7b934000004000000000000b7c734000104000000000000c5d534000204000000000000d5e334000304000000000000e3f134000404000000000000f3ff34000504000000000000030e35000604000000000000121c350007040000000000001d2a350008040000000000002531350009040000000000001f3835000a040000000000000c4635000b04000000000000f35335000c04000000000000016235000d040000000000000e7035000e040000000000001e7e35000f040000000000002e8c350010040000000000003c9a350011040000000000002da83500120400000000000022b63500130400000000000032c43500140400000000000041d23500150400000000000050e0350016040000000000005eee350017040000000000006efc350018040000000000002608360019040000000000007e0a36001a040000000000008b1836001b040000000000009b2636001c04000000000000aa3436001d04000000000000ba4236001e04000000000000ca5036001f04000000000000d95e360020040000000000009b6c36002104000000000000c97936002204000000000000488036002304000000000000198736002404000000000000a38b36002504000000000000f79436002604000000000000e4a236002704000000000000d6b036002804000000000000d0be36002904000000000000bfcc36002a04000000000000bdda36002b04000000000000c4e836002c04000000000000c7f636002d04000000000000d10437002e04000000000000da1237002f04000000000000df2037003004000000000000de2e37003104000000000000e03c37003204000000000000e84a37003304000000000000f15837003404000000000000495b37003504000000000000a15d370036040000000000000067370037040000000000000775370038040000000000000c8337003904000000000000fb9037003a040000000000006d9c37003b04000000000000bf9e37003c040000000000006da337003d04000000000000cbac37003e04000000000000d8ba37003f04000000000000e3c837004004000000000000edd637004104000000000000f7e43700420400000000000006f337004304000000000000150138004404000000000000200f380045040000000000002d1d38004604000000000000342b38004704000000000000423938004804000000000000074738004904000000000000ae5438004a04000000000000516238004b04000000000000ef6f38004c040000000000009a7d38004d04000000000000d77f38004e04000000000000878b38004f0400000000000091993800500400000000000098a738005104000000000000a1b538005204000000000000abc338005304000000000000b1d138005404000000000000b6df38005504000000000000bfed38005604000000000000c8fb38005704000000000000d60939005804000000000000db1039005904000000000000e01739005a04000000000000e82539005b04000000000000ef3339005c04000000000000e24139005d04000000000000e54f39005e04000000000000ef5d39005f04000000000000f96b39006004000000000000067a390061040000000000001288390062040000000000002096390063040000000000002da43900640400000000000038b23900650400000000000041c0390066040000000000004dce3900670400000000000059dc3900680400000000000066ea390069040000000000006bf839006a0400000000000077063a006b0400000000000080143a006c040000000000008a223a006d0400000000000093303a006e04000000000000ef393a006f040000000000009a3e3a007004000000000000a74c3a007104000000000000ae5a3a007204000000000000ba683a007304000000000000c7763a007404000000000000b7843a007504000000000000be923a0076040000000000009fa03a007704000000000000a3ae3a007804000000000000a6bc3a007904000000000000a6ca3a007a0400000000000090d83a007b0400000000000002e63a007c04000000000000d9f33a007d04000000000000db013b007e0400000000000033043b007f04000000000000eb0f3b0080040000000000009b143b008104000000000000f3163b008204000000000000fb1d3b0083040000000000000a2c3b0084040000000000001a3a3b0085040000000000002a483b00860400000000000039563b00870400000000000049643b0088040000000000004f723b0089040000000000005f803b008a0400000000000067873b008b040000000000006f8e3b008c04000000000000c7903b008d040000000000007f9c3b008e040000000000008faa3b008f040000000000008ab83b0090040000000000008dc63b009104000000000000e0c83b00920400000000000090cd3b00930400000000000098d43b009404000000000000a8e23b009504000000000000b6f03b009604000000000000befe3b009704000000000000c50c3c009804000000000000cd1a3c0099040000000000002d243c009a04000000000000dd283c009b04000000000000ed363c009c04000000000000fc443c009d040000000000000c533c009e040000000000001b613c009f040000000000002a6f3c00a0040000000000003a7d3c00a1040000000000004a8b3c00a20400000000000059993c00a30400000000000069a73c00a40400000000000079b53c00a50400000000000089c33c00a60400000000000099d13c00a704000000000000a8df3c00a804000000000000a5ed3c00a904000000000000b5fb3c00aa04000000000000c2093d00ab04000000000000ca173d00ac04000000000000da253d00ad04000000000000ea333d00ae04000000000000fa413d00af040000000000000a503d00b0040000000000001a5e3d00b1040000000000002a6c3d00b2040000000000003a7a3d00b3040000000000004a883d00b4040000000000005a963d00b5040000000000006aa43d00b6040000000000007ab23d00b7040000000000008ac03d00b8040000000000009ace3d00b904000000000000aadc3d00ba04000000000000baea3d00bb04000000000000caf83d00bc04000000000000da063e00bd04000000000000e9143e00be04000000000000f9223e00bf0400000000000009313e00c004000000000000193f3e00c104000000000000294d3e00c204000000000000395b3e00c30400000000000049693e00c40400000000000058773e00c50400000000000068853e00c60400000000000078933e00c70400000000000086a13e00c80400000000000096af3e00c904000000000000a6bd3e00ca04000000000000b6cb3e00cb04000000000000c6d93e00cc04000000000000d6e73e00cd04000000000000e5f53e00ce04000000000000f5033f00cf0400000000000005123f00d00400000000000015203f00d104000000000000252e3f00d204000000000000353c3f00d304000000000000454a3f00d40400000000000055583f00d5040000000000005d663f00d6040000000000006d743f00d7040000000000007d823f00d80400000000000085903f00d904000000000000959e3f00da04000000000000a5ac3f00db04000000000000b4ba3f00dc04000000000000c4c83f00dd04000000000000d4d63f00de04000000000000e4e43f00df0400000000000044ee3f00e004000000000000f4f23f00e10400000000000004014000e204000000000000140f4000e304000000000000241d4000e404000000000000342b4000e50400000000000044394000e60400000000000054474000e70400000000000064554000e80400000000000074634000e90400000000000084714000ea040000000000008d7f4000eb040000000000009b8d4000ec04000000000000aa9b4000ed04000000000000baa94000ee04000000000000cab74000ef04000000000000dac54000f004000000000000e9d34000f104000000000000f3e14000f20400000000000003f04000f30400000000000013fe4000f404000000000000230c4100f504000000000000331a4100f60400000000000043284100f70400000000000051364100f8040000000000005f444100f9040000000000006e524100fa040000000000007e604100fb040000000000008c6e4100fc040000000000009c7c4100fd04000000000000ac8a4100fe04000000000000bc984100ff04000000000000cca641000005000000000000dab441000105000000000000eac241000205000000000000f9d04100030500000000000009df4100040500000000000019ed4100050500000000000029fb410006050000000000003909420007050000000000003b17420008050000000000003a25420009050000000000003d3342000a05000000000000304142000b05000000000000304f42000c05000000000000295d42000d05000000000000246b42000e05000000000000297942000f050000000000002587420010050000000000002295420011050000000000001aa3420012050000000000001bb14200130500000000000019bf420014050000000000000ccd42001505000000000000f5da42001605000000000000e5e842001705000000000000c8f642001805000000000000c50443001905000000000000c31243001a05000000000000c62043001b05000000000000c42e43001c05000000000000bb3c43001d05000000000000b74a43001e05000000000000b25843001f05000000000000ac6643002005000000000000177443002105000000000000158143002205000000000000118e430023050000000000000c9b430024050000000000000da84300250500000000000013b54300260500000000000019c24300270500000000000031cf4300280500000000000022dc4300290500000000000046e943002a0500000000000051f643002b05000000000000620344002c050000000000006b1044002d050000000000007c1d44002e05000000000000962a44002f05000000000000a537440030050000000000007545440031050000000000006f53440032050000000000006e6144003305000000000000676f44003405000000000000697d44003505000000000000668b440036050000000000006099440037050000000000005fa74400380500000000000062b5440039050000000000005cc344003a0500000000000061d144003b0500000000000053df44003c0500000000000054ed44003d0500000000000055fb44003e05000000000000560945003f050000000000004e17450040050000000000004f2545004105000000000000a527450042050000000000004b33450043050000000000004241450044050000000000003e4f45004505000000000000315d450046050000000000002c6b450047050000000000002679450048050000000000001d8745004905000000000000129545004a0500000000000011a345004b0500000000000013b145004c050000000000000bbf45004d0500000000000008cd45004e050000000000000adb45004f0500000000000006e94500500500000000000003f745005105000000000000ff0446005205000000000000f81246005305000000000000ef20460054050000000000003f2a46005505000000000000e72e46005605000000000000eb3c46005705000000000000e54a460058050000000000003d5446005905000000000000ed5846005a05000000000000fd6646005b050000000000000d7546005c050000000000001d8346005d050000000000002d9146005e050000000000003d9f46005f050000000000004cad460060050000000000005cbb460061050000000000006bc94600620500000000000079d74600630500000000000089e54600640500000000000094f346006505000000000000a30147006605000000000000b30f47006705000000000000c21d47006805000000000000d22b47006905000000000000e23947006a05000000000000f24747006b05000000000000025647006c05000000000000126447006d05000000000000227247006e05000000000000318047006f05000000000000418e47007005000000000000519c4700710500000000000061aa4700720500000000000071b84700730500000000000081c64700740500000000000090d4470075050000000000009fe247007605000000000000adf047007705000000000000bdfe47007805000000000000cd0c48007905000000000000dd1a48007a05000000000000ed2848007b05000000000000fd3648007c050000000000000d4548007d050000000000001d5348007e050000000000002d6148007f050000000000003d6f480080050000000000004d7d480081050000000000005c8b480082050000000000006c99480083050000000000006ea74800840500000000000076b54800850500000000000086c34800860500000000000096d148008705000000000000a3df48008805000000000000b3ed48008905000000000000c3fb48008a05000000000000d30949008b05000000000000e31749008c05000000000000f32549008d05000000000000013449008e050000000000000e4249008f050000000000001e50490090050000000000002d5e490091050000000000003d6c490092050000000000004d7a490093050000000000005c88490094050000000000006b96490095050000000000007aa44900960500000000000089b24900970500000000000099c049009805000000000000a9ce49009905000000000000b8dc49009a050000000000009cea49009b0500000000000071f849009c0500000000000081064a009d0500000000000091144a009e05000000000000a1224a009f05000000000000b1304a00a005000000000000c03e4a00a105000000000000d04c4a00a205000000000000e05a4a00a305000000000000f0684a00a40500000000000000774a00a5050000000000000e854a00a6050000000000001e934a00a7050000000000002da14a00a8050000000000003baf4a00a9050000000000004abd4a00aa0500000000000059cb4a00ab0500000000000069d94a00ac0500000000000078e74a00ad0500000000000088f54a00ae0500000000000092034b00af05000000000000a1114b00b005000000000000b01f4b00b105000000000000c02d4b00b205000000000000d03b4b00b305000000000000df494b00b405000000000000ef574b00b505000000000000fd654b00b60500000000000055684b00b7050000000000000c744b00b805000000000000147b4b00b9050000000000006c7d4b00ba050000000000001c824b00bb0500000000000029904b00bc05000000000000369e4b00bd0500000000000045ac4b00be0500000000000050ba4b00bf05000000000000b0c34b00c00500000000000060c84b00c1050000000000006fd64b00c2050000000000007fe44b00c3050000000000008ff24b00c4050000000000009e004c00c505000000000000ad0e4c00c605000000000000bc1c4c00c705000000000000cc2a4c00c805000000000000db384c00c905000000000000eb464c00ca05000000000000f7544c00cb0500000000000007634c00cc0500000000000017714c00cd05000000000000247f4c00ce05000000000000348d4c00cf05000000000000449b4c00d00500000000000054a94c00d1050000000000005fb74c00d2050000000000006fc54c00d3050000000000007ed34c00d4050000000000008ee14c00d5050000000000009eef4c00d605000000000000acfd4c00d705000000000000b90b4d00d805000000000000c9194d00d905000000000000d9274d00da05000000000000e9354d00db05000000000000f9434d00dc0500000000000009524d00dd0500000000000019604d00de05000000000000296e4d00df05000000000000377c4d00e005000000000000158a4d00e10500000000000030974d00e20500000000000067a44d00e30500000000000074b24d00e40500000000000083c04d00e50500000000000093ce4d00e605000000000000a3dc4d00e705000000000000b3ea4d00e805000000000000c3f84d00e905000000000000d3064e00ea05000000000000e1144e00eb05000000000000f1224e00ec05000000000000a92e4e00ed0500000000000001314e00ee05000000000000113f4e00ef0500000000000019464e00f005000000000000174d4e00f105000000000000255b4e00f20500000000000035694e00f30500000000000045774e00f40500000000000053854e00f50500000000000062934e00f60500000000000070a14e00f70500000000000080af4e00f805000000000000d8b14e00f90500000000000090bd4e00fa05000000000000a0cb4e00fb05000000000000b0d94e00fc05000000000000bfe74e00fd05000000000000cdf54e00fe05000000000000dd034f00ff05000000000000ed114f000006000000000000fc1f4f00010600000000000003274f0002060000000000000a2e4f0003060000000000001a3c4f0004060000000000002a4a4f00050600000000000039584f00060600000000000049664f00070600000000000057744f00080600000000000067824f00090600000000000077904f000a06000000000000869e4f000b0600000000000096ac4f000c06000000000000a6ba4f000d06000000000000b3c84f000e06000000000000c3d64f000f06000000000000c8e44f001006000000000000d5f24f001106000000000000e50050001206000000000000f30e50001306000000000000021d500014060000000000000a2b50001506000000000000113950001606000000000000214750001706000000000000315550001806000000000000416350001906000000000000507150001a06000000000000607f50001b06000000000000708d50001c060000000000007f9b50001d060000000000008fa950001e0600000000000099b750001f06000000000000a8c550002006000000000000b8d350002106000000000000c8e15000220600000000000078e650002306000000000000cbef50002406000000000000dafd50002506000000000000e90b51002606000000000000f81951002706000000000000072851002806000000000000173651002906000000000000254451002a06000000000000345251002b06000000000000446051002c06000000000000536e51002d06000000000000637c51002e06000000000000728a51002f0600000000000070985100300600000000000065a6510031060000000000005bb45100320600000000000049c25100330600000000000032d05100340600000000000018de5100350600000000000009ec51003606000000000000f5f951003706000000000000e00752003806000000000000c41552003906000000000000a92352003a06000000000000913152003b06000000000000743f52003c060000000000005a4d52003d06000000000000585b52003e06000000000000506952003f0600000000000049775200400600000000000041855200410600000000000030935200420600000000000017a15200430600000000000007af52004406000000000000f4bc52004506000000000000deca52004606000000000000b7d85200470600000000000095e6520048060000000000007ff4520049060000000000006c0253004a06000000000000611053004b06000000000000421e53004c06000000000000282c53004d06000000000000063a53004e06000000000000f44753004f06000000000000dd5553005006000000000000ca6353005106000000000000cb7153005206000000000000b17f53005306000000000000978d53005406000000000000799b530055060000000000005fa95300560600000000000053b75300570600000000000039c5530058060000000000002ed35300590600000000000010e153005a06000000000000fcee53005b06000000000000f4f553005c06000000000000edfc53005d06000000000000dc0a54005e06000000000000b01854005f060000000000008026540060060000000000005b34540061060000000000004d4254006206000000000000325054006306000000000000225e540064060000000000000b6c54006506000000000000f37954006606000000000000df8754006706000000000000c79554006806000000000000ada35400690600000000000090b154006a060000000000007bbf54006b0600000000000064cd54006c0600000000000049db54006d0600000000000044e954006e060000000000002df754006f060000000000000b0555007006000000000000f51255007106000000000000d92055007206000000000000cc2e55007306000000000000c63c55007406000000000000c44a55007506000000000000c45855007606000000000000c76655007706000000000000c17455007806000000000000b88255007906000000000000b59055007a06000000000000b49e55007b06000000000000afac55007c06000000000000b3ba55007d06000000000000b6c855007e06000000000000b1d655007f06000000000000aae45500800600000000000092f2550081060000000000009200560082060000000000008f0e56008306000000000000891c56008406000000000000792a560085060000000000007738560086060000000000007446560087060000000000006b54560088060000000000006362560089060000000000005e7056008a06000000000000597e56008b06000000000000548c56008c06000000000000519a56008d060000000000004fa856008e060000000000003bb656008f0600000000000035c45600900600000000000030d2560091060000000000002be0560092060000000000002aee560093060000000000002bfc560094060000000000001e0a57009506000000000000181857009606000000000000142657009706000000000000083457009806000000000000084257009906000000000000095057009a060000000000000a5e57009b06000000000000076c57009c06000000000000fe7957009d06000000000000f78757009e06000000000000f49557009f06000000000000f5a35700a006000000000000f3b15700a106000000000000f5bf5700a206000000000000f3cd5700a306000000000000eddb5700a406000000000000e5e95700a506000000000000eaf75700a606000000000000ef055800a706000000000000ee135800a806000000000000e4215800a906000000000000e12f5800aa06000000000000e63d5800ab06000000000000e54b5800ac06000000000000e3595800ad06000000000000d1675800ae06000000000000c9755800af06000000000000cd835800b006000000000000bb915800b106000000000000b19f5800b206000000000000abad5800b306000000000000a2bb5800b40600000000000099c95800b5060000000000008cd75800b60600000000000080e55800b7060000000000007cf35800b80600000000000074015900b906000000000000740f5900ba06000000000000671d5900bb060000000000005d2b5900bc0600000000000054395900bd0600000000000049475900be060000000000003e555900bf0600000000000032635900c00600000000000025715900c106000000000000207f5900c206000000000000068d5900c306000000000000f79a5900c406000000000000efa85900c506000000000000ebb65900c606000000000000ebc45900c706000000000000ebd25900c806000000000000ede05900c906000000000000f2ee5900ca06000000000000fffc5900cb060000000000000d0b5a00cc060000000000001c195a00cd0600000000000027275a00ce0600000000000037355a00cf0600000000000046435a00d00600000000000055515a00d106000000000000635f5a00d206000000000000716d5a00d306000000000000817b5a00d40600000000000090895a00d5060000000000009f975a00d606000000000000ada55a00d706000000000000bdb35a00d806000000000000ccc15a00d906000000000000d7cf5a00da06000000000000e6dd5a00db06000000000000f5eb5a00dc0600000000000004fa5a00dd0600000000000012085b00de0600000000000022165b00df0600000000000032245b00e0060000000000003f325b00e1060000000000004e405b00e2060000000000005b4e5b00e306000000000000695c5b00e406000000000000bf5e5b00e50600000000000015615b00e606000000000000746a5b00e70600000000000084785b00e80600000000000092865b00e906000000000000a0945b00ea06000000000000ada25b00eb06000000000000b9b05b00ec06000000000000c2be5b00ed06000000000000d2cc5b00ee06000000000000e1da5b00ef06000000000000f0e85b00f006000000000000fef65b00f1060000000000000e055c00f20600000000000012135c00f3060000000000001a215c00f406000000000000172f5c00f5060000000000001f3d5c00f6060000000000002b4b5c00f70600000000000037595c00f80600000000000044675c00f90600000000000053755c00fa060000000000005f835c00fb060000000000006b915c00fc06000000000000769f5c00fd0600000000000080ad5c00fe0600000000000089bb5c00ff0600000000000040c75c00000700000000000097c95c000107000000000000a5d75c000207000000000000b2e55c000307000000000000bef35c000407000000000000cd015d000507000000000000dd0f5d000607000000000000df1d5d000707000000000000ee2b5d000807000000000000fa395d00090700000000000009485d000a0700000000000013565d000b0700000000000022645d000c0700000000000030725d000d070000000000003f805d000e070000000000004e8e5d000f070000000000005d9c5d0010070000000000006caa5d00110700000000000079b85d0012070000000000007dc65d0013070000000000008cd45d00140700000000000082e25d00150700000000000084f05d00160700000000000086fe5d001707000000000000920c5e0018070000000000009f1a5e001907000000000000a3285e001a07000000000000a7365e001b070000000000009f445e001c07000000000000ac525e001d07000000000000b9605e001e07000000000000c76e5e001f07000000000000647c5e002007000000000000fd895e002107000000000000bb905e00220700000000000004935e002307000000000000ad975e002407000000000000afa55e002507000000000000bdb35e002607000000000000cdc15e002707000000000000dbcf5e002807000000000000dcdd5e002907000000000000d9eb5e002a07000000000000daf95e002b07000000000000dc075f002c07000000000000df155f002d07000000000000d4235f002e07000000000000d4315f002f07000000000000d03f5f003007000000000000cc4d5f003107000000000000b85b5f003207000000000000c7695f003307000000000000d6775f003407000000000000e4855f003507000000000000f4935f00360700000000000004a25f00370700000000000014b05f00380700000000000024be5f00390700000000000034cc5f003a0700000000000043da5f003b0700000000000052e85f003c0700000000000062f65f003d07000000000000700460003e070000000000007f1260003f070000000000008f20600040070000000000009d2e60004107000000000000ad3c60004207000000000000bb4a60004307000000000000ca5860004407000000000000da6660004507000000000000e57460004607000000000000f48260004707000000000000039160004807000000000000129f6000490700000000000021ad60004a0700000000000030bb60004b0700000000000040c960004c070000000000004fd760004d070000000000005fe560004e070000000000006cf360004f070000000000007c0161005007000000000000880f61005107000000000000961d61005207000000000000a52b61005307000000000000b33961005407000000000000c34761005507000000000000d355610056070000000000009f63610057070000000000005371610058070000000000005c7f610059070000000000006c8d61005a07000000000000789b61005b0700000000000088a961005c0700000000000098b761005d07000000000000a7c561005e07000000000000b7d361005f07000000000000c6e16100600700000000000081ef6100610700000000000021fd61006207000000000000bc0a62006307000000000000601862006407000000000000082662006507000000000000943362006607000000000000374162006707000000000000d14e62006807000000000000805c620069070000000000002e6a62006a07000000000000dc7762006b070000000000007b8562006c070000000000000a9362006d07000000000000aaa062006e0700000000000047ae62006f07000000000000d5bb620070070000000000009ec962007107000000000000aed762007207000000000000bee562007307000000000000cdf362007407000000000000da0163007507000000000000e30f63007607000000000000f21d63007707000000000000022c63007807000000000000103a63007907000000000000204863007a07000000000000305663007b07000000000000406463007c07000000000000507263007d07000000000000608063007e07000000000000688e63007f07000000000000739c6300800700000000000083aa6300810700000000000092b863008207000000000000a0c66300830700000000000050cb63008407000000000000a8cd63008507000000000000b0d4630086070000000000000ede63008707000000000000bee263008807000000000000cef063008907000000000000c1fe63008a07000000000000d10c64008b07000000000000e01a64008c07000000000000f02864008d07000000000000ff3664008e070000000000000f4564008f070000000000001e53640090070000000000002d6164009107000000000000236f64009207000000000000297d64009307000000000000d28164009407000000000000308b64009507000000000000409964009607000000000000f09d640097070000000000004fa76400980700000000000057ae64009907000000000000afb064009a070000000000005eb564009b070000000000006ec364009c0700000000000079d164009d0700000000000088df64009e0700000000000094ed64009f07000000000000a3fb6400a007000000000000b0096500a107000000000000bb176500a207000000000000111a6500a307000000000000c6256500a407000000000000262f6500a507000000000000d6336500a607000000000000e4416500a707000000000000824d6500a807000000000000d34f6500a907000000000000cc5d6500aa070000000000001f606500ab07000000000000ca6b6500ac07000000000000c9796500ad070000000000001c836500ae07000000000000c7876500af07000000000000c6956500b007000000000000bca36500b10700000000000063af6500b207000000000000b5b16500b307000000000000b1bf6500b40700000000000056c46500b507000000000000a3cd6500b607000000000000a7db6500b707000000000000a4e96500b8070000000000004cf56500b907000000000000a3f76500ba070000000000009c056600bb070000000000009c136600bc0700000000000094216600bd07000000000000a12f6600be0700000000000000396600bf07000000000000b03d6600c007000000000000be4b6600c107000000000000cd596600c207000000000000dd676600c307000000000000ed756600c407000000000000fd836600c5070000000000000d926600c6070000000000001da06600c7070000000000002dae6600c8070000000000003dbc6600c9070000000000004dca6600ca070000000000005cd86600cb070000000000006ce66600cc0700000000000079f46600cd0700000000000088026700ce0700000000000097106700cf07000000000000a71e6700d007000000000000b62c6700d107000000000000c53a6700d207000000000000d4486700d307000000000000e3566700d407000000000000f3646700d50700000000000001736700d60700000000000011816700d707000000000000218f6700d807000000000000319d6700d9070000000000003dab6700da070000000000004cb96700db0700000000000058c76700dc0700000000000066d56700dd0700000000000075e36700de0700000000000082f16700df0700000000000090ff6700e0070000000000009d0d6800e107000000000000aa1b6800e207000000000000b4296800e307000000000000c2376800e407000000000000c5456800e507000000000000a8536800e60700000000000093616800e7070000000000007d6f6800e807000000000000607d6800e9070000000000005f8b6800ea070000000000005e996800eb0700000000000056a76800ec070000000000004fb56800ed0700000000000040c36800ee0700000000000033d16800ef0700000000000024df6800f0070000000000000bed6800f107000000000000f8fa6800f207000000000000d6086900f307000000000000b3166900f40700000000000090246900f5070000000000008c326900f6070000000000009b406900f707000000000000ab4e6900f807000000000000b45c6900f907000000000000c36a6900fa07000000000000d3786900fb07000000000000e1866900fc07000000000000f1946900fd0700000000000001a36900fe0700000000000011b16900ff0700000000000021bf690000080000000000002fcd6900010800000000000036d4690002080000000000003edb690003080000000000004ee9690004080000000000005ef7690005080000000000006e056a0006080000000000007e136a0007080000000000008e216a0008080000000000009e2f6a000908000000000000ad3d6a000a08000000000000bc4b6a000b08000000000000cc596a000c08000000000000dc676a000d08000000000000eb756a000e08000000000000fb836a000f080000000000000b926a0010080000000000001ba06a00110800000000000029ae6a00120800000000000038bc6a0013080000000000003fca6a0014080000000000004ed86a0015080000000000005ee66a0016080000000000006bf46a00170800000000000079026b0018080000000000007b106b0019080000000000008b1e6b001a08000000000000962c6b001b08000000000000a53a6b001c08000000000000b5486b001d08000000000000b8566b001e08000000000000ba646b001f08000000000000c5726b002008000000000000d2806b002108000000000000dc8e6b002208000000000000e3956b002308000000000000ea9c6b002408000000000000faaa6b0025080000000000000ab96b00260800000000000019c76b00270800000000000029d56b00280800000000000037e36b00290800000000000046f16b002a0800000000000056ff6b002b08000000000000640d6c002c080000000000006d1b6c002d0800000000000079296c002e0800000000000087376c002f0800000000000097456c003008000000000000a7536c003108000000000000b7616c003208000000000000c66f6c0033080000000000001e726c003408000000000000d67d6c0035080000000000008e896c003608000000000000e68b6c003708000000000000f5996c00380800000000000005a86c00390800000000000011b66c003a080000000000000ac46c003b08000000000000b5d16c003c080000000000008ddf6c003d0800000000000097ed6c003e08000000000000a7fb6c003f08000000000000b7096d004008000000000000c6176d004108000000000000d6256d004208000000000000e6336d004308000000000000f6416d00440800000000000006506d004508000000000000155e6d004608000000000000256c6d004708000000000000357a6d00480800000000000045886d00490800000000000055966d004a0800000000000065a46d004b0800000000000074b26d004c0800000000000084c06d004d0800000000000094ce6d004e08000000000000a4dc6d004f08000000000000b4ea6d005008000000000000c4f86d005108000000000000d4066e005208000000000000e4146e005308000000000000f4226e00540800000000000004316e005508000000000000143f6e005608000000000000244d6e005708000000000000345b6e00580800000000000043696e00590800000000000053776e005a08000000000000037c6e005b0800000000000063856e005c08000000000000138a6e005d0800000000000073936e005e0800000000000083a16e005f0800000000000091af6e00600800000000000098b66e006108000000000000a0bd6e006208000000000000b0cb6e006308000000000000bfd96e006408000000000000cfe76e006508000000000000dff56e006608000000000000ef036f006708000000000000ff116f0068080000000000000e206f0069080000000000001d2e6f006a080000000000002c3c6f006b080000000000003b4a6f006c0800000000000049586f006d0800000000000059666f006e0800000000000068746f006f0800000000000078826f00700800000000000088906f007108000000000000989e6f007208000000000000a6ac6f007308000000000000b5ba6f007408000000000000c3c86f007508000000000000d3d66f007608000000000000e3e46f007708000000000000f3f26f007808000000000000010170007908000000000000110f70007a08000000000000211d70007b080000000000002f2b70007c080000000000003f3970007d080000000000004f4770007e080000000000005f5570007f080000000000006f63700080080000000000007f71700081080000000000008f7f700082080000000000009e8d70008308000000000000ae9b70008408000000000000bea970008508000000000000ceb770008608000000000000dec570008708000000000000eed370008808000000000000fce1700089080000000000000cf070008a080000000000001cfe70008b080000000000002c0c71008c080000000000003c1a71008d080000000000004c2871008e08000000000000553671008f08000000000000654471009008000000000000755271009108000000000000856071009208000000000000956e71009308000000000000a57c71009408000000000000b48a71009508000000000000c49871009608000000000000d2a671009708000000000000e1b471009808000000000000c1c271009908000000000000d1d071009a08000000000000e1de71009b08000000000000f1ec71009c08000000000000fefa71009d080000000000000e0972009e080000000000001e1772009f080000000000002d257200a0080000000000003d337200a1080000000000004d417200a2080000000000005d4f7200a3080000000000006d5d7200a4080000000000006c6b7200a5080000000000007a797200a6080000000000008a877200a7080000000000009a957200a808000000000000aaa37200a908000000000000bab17200aa08000000000000cabf7200ab08000000000000d9cd7200ac08000000000000e9db7200ad08000000000000f9e97200ae0800000000000008f87200af0800000000000018067300b00800000000000028147300b10800000000000033227300b20800000000000043307300b308000000000000513e7300b408000000000000614c7300b508000000000000645a7300b60800000000000060687300b70800000000000059767300b80800000000000068847300b90800000000000078927300ba0800000000000088a07300bb0800000000000097ae7300bc080000000000008fbc7300bd0800000000000090ca7300be0800000000000086d87300bf0800000000000085e67300c00800000000000089f47300c10800000000000084027400c20800000000000088107400c3080000000000008d1e7400c4080000000000008e2c7400c5080000000000008a3a7400c6080000000000009a487400c708000000000000aa567400c808000000000000ba647400c908000000000000ca727400ca08000000000000da807400cb08000000000000ea8e7400cc08000000000000fa9c7400cd08000000000000feaa7400ce080000000000000eb97400cf080000000000001ec77400d0080000000000002ed57400d1080000000000003ee37400d2080000000000004ef17400d3080000000000005eff7400d4080000000000006a0d7500d508000000000000791b7500d60800000000000089297500d70800000000000098377500d80800000000000050437500d908000000000000a8457500da08000000000000b8537500db08000000000000c8617500dc08000000000000d86f7500dd08000000000000e87d7500de08000000000000f88b7500df08000000000000059a7500e00800000000000015a87500e10800000000000025b67500e20800000000000034c47500e30800000000000044d27500e40800000000000054e07500e50800000000000064ee7500e60800000000000074fc7500e708000000000000840a7600e80800000000000091187600e908000000000000a1267600ea08000000000000b1347600eb08000000000000c1427600ec08000000000000d1507600ed08000000000000cb5e7600ee0800000000000023617600ef08000000000000db6c7600f008000000000000eb7a7600f108000000000000fa887600f2080000000000000a977600f3080000000000001aa57600f4080000000000002ab37600f5080000000000003ac17600f60800000000000049cf7600f70800000000000059dd7600f80800000000000069eb7600f90800000000000079f97600fa0800000000000089077700fb0800000000000099157700fc08000000000000a9237700fd08000000000000b9317700fe08000000000000c73f7700ff08000000000000d64d77000009000000000000e65b77000109000000000000f6697700020900000000000005787700030900000000000015867700040900000000000024947700050900000000000034a27700060900000000000044b07700070900000000000054be7700080900000000000064cc7700090900000000000074da77000a0900000000000084e877000b0900000000000094f677000c09000000000000a40478000d09000000000000b41278000e09000000000000c42078000f09000000000000d42e78001009000000000000e43c78001109000000000000f44a78001209000000000000045978001309000000000000146778001409000000000000247578001509000000000000348378001609000000000000449178001709000000000000529f7800180900000000000062ad780019090000000000004abb78001a0900000000000058c978001b0900000000000068d778001c0900000000000074e578001d0900000000000083f378001e09000000000000eb0379001f09000000000000531479002009000000000000bb24790021090000000000002235790022090000000000008a4579002309000000000000f255790024090000000000005a6679002509000000000000c276790026090000000000002a8779002709000000000000919779002809000000000000f8a77900290900000000000050b879002a09000000000000b2c879002b0900000000000017d979002c0900000000000079e979002d09000000000000d9f979002e090000000000003c0a7a002f09000000000000a21a7a003009000000000000092b7a003109000000000000713b7a003209000000000000d84b7a0033090000000000003e5c7a003409000000000000a56c7a0035090000000000000d7d7a003609000000000000758d7a003709000000000000dd9d7a00380900000000000045ae7a003909000000000000adbe7a003a0900000000000014cf7a003b0900000000000060df7a003c09000000000000a3ef7a003d09000000000000e3ff7a003e09000000000000e60f7b003f09000000000000dd1f7b00400900000000000000297b004109000000000000f52f7b00420900000000000000407b004309000000000000d24f7b004409000000000000a35f7b004509000000000000656f7b004609000000000000607f7b0047090000000000006e8f7b0048090000000000005a9f7b00490900000000000016af7b004a0900000000000009b87b004b09000000000000c9be7b004c0900000000000095ce7b004d090000000000006ade7b004e0900000000000039ee7b004f090000000000001afe7b005009000000000000140e7c005109000000000000191e7c005209000000000000212e7c0053090000000000002a3e7c005409000000000000044e7c005509000000000000ff5d7c005609000000000000116e7c005709000000000000397e7c005809000000000000608e7c005909000000000000909e7c005a09000000000000cdae7c005b09000000000000e4be7c005c09000000000000fcce7c005d0900000000000013df7c005e0900000000000043ef7c005f0900000000000067ff7c006009000000000000890f7d0061090000000000000e1b7d006209000000000000af1f7d006309000000000000882d7d006409000000000000d62f7d00650900000000000005407d0066090000000000009c447d00670900000000000026507d00680900000000000055607d00690900000000000074707d006a0900000000000096807d006b09000000000000ba907d006c09000000000000dda07d006d0900000000000004b17d006e090000000000002bc17d006f0900000000000053d17d0070090000000000008ae17d007109000000000000bff17d007209000000000000f3017e00730900000000000023127e007409000000000000a81d7e0075090000000000004c227e0076090000000000008f327e007709000000000000cf427e00780900000000000011537e0079090000000000004e637e007a090000000000008f737e007b09000000000000cb837e007c09000000000000e8937e007d09000000000000aba17e007e09000000000000f7a37e007f090000000000002fb47e00800900000000000068c47e008109000000000000a3d47e008209000000000000e2e47e00830900000000000020f57e00840900000000000061057f008509000000000000050a7f008609000000000000a2157f008709000000000000e4257f0088090000000000001c367f0089090000000000005c467f008a0900000000000097567f008b09000000000000ce667f008c090000000000000e777f008d0900000000000047877f008e0900000000000083977f008f09000000000000c7a77f00900900000000000007b87f00910900000000000040c87f00920900000000000074d87f009309000000000000b2e87f009409000000000000eef87f0095090000000000002709800096090000000000006619800097090000000000005b2080009809000000000000bb2980009909000000000000233a80009a09000000000000554a80009b09000000000000aa5a80009c09000000000000025d80009d09000000000000016b80009e09000000000000697b80009f09000000000000d08b8000a009000000000000389c8000a109000000000000a0ac8000a20900000000000008bd8000a3090000000000006fcd8000a409000000000000d7dd8000a5090000000000003fee8000a609000000000000a7fe8000a709000000000000af058100a8090000000000000f0f8100a909000000000000771f8100aa09000000000000df2f8100ab0900000000000047408100ac09000000000000ae508100ad0900000000000016618100ae090000000000007e718100af090000000000002e768100b009000000000000c1818100b109000000000000a08f8100b209000000000000f3918100b30900000000000019a28100b40900000000000057b28100b509000000000000bfc28100b60900000000000027d38100b7090000000000008fe38100b809000000000000c0f38100b90900000000000003048200ba090000000000003c148200bb0900000000000079248200bc09000000000000af348200bd09000000000000ef448200be090000000000002b558200bf0900000000000062658200c00900000000000094758200c1090000000000006f858200c20900000000000096908200c30900000000000034958200c40900000000000063a58200c50900000000000057b38200c60900000000000090c38200c709000000000000cdd38200c8090000000000000ce48200c90900000000000051f48200ca0900000000000086048300cb09000000000000cf148300cc0900000000000018258300cd09000000000000b4298300ce09000000000000572e8300cf09000000000000076d8300d00900000000000054a28300d109000000000000d40a8400d20900000000000092208500d309000000000000f6308500d40900000000000059418500d509000000000000bf518500d609000000000000856b8500d709000000000000eb7b8500d809000000000000528c8500d909000000000000af9c8500da0900000000000013ad8500db090000000000007bbd8500dc09000000000000e3cd8500dd090000000000004bde8500de09000000000000b3ee8500df090000000000001bff8500e009000000000000820f8600e109000000000000ea1f8600e20900000000000052308600e309000000000000b3408600e409000000000000bb478600e5090000000000001b518600e6090000000000007f618600e709000000000000e7718600e8090000000000004c828600e909000000000000b4928600ea090000000000001ca38600eb0900000000000084b38600ec09000000000000dec38600ed090000000000002ed48600ee0900000000000086e48600ef09000000000000d7f48600f00900000000000028058700f1090000000000006d158700f2090000000000009d258700f309000000000000dc358700f40900000000000014468700f50900000000000056568700f609000000000000a8668700f709000000000000f4768700f8090000000000004f878700f90900000000000098978700fa09000000000000eba78700fb0900000000000043b88700fc0900000000000093c88700fd09000000000000f9d88700fe0900000000000061e98700ff09000000000000c9f98700000a000000000000300a8800010a000000000000981a8800020a000000000000ff2a8800030a000000000000673b8800040a000000000000cf4b8800050a000000000000375c8800060a0000000000009e6c8800070a000000000000067d8800080a0000000000006d8d8800090a000000000000cf9d88000a0a00000000000037ae88000b0a0000000000009fbe88000c0a00000000000007cf88000d0a0000000000006fdf88000e0a000000000000d1ef88000f0a00000000000039008900100a000000000000a1108900110a00000000000009218900120a00000000000071318900130a000000000000d9418900140a00000000000041528900150a000000000000a6628900160a000000000000b6708900170a0000000000000d738900180a00000000000075838900190a000000000000dd9389001a0a00000000000044a489001b0a000000000000a4ad89001c0a000000000000acb489001d0a00000000000013c589001e0a00000000000078d589001f0a000000000000dee58900200a00000000000044f68900210a000000000000ab068a00220a00000000000013178a00230a00000000000079278a00240a000000000000dc378a00250a0000000000003f488a00260a000000000000a6588a00270a00000000000008698a00280a00000000000007778a00290a0000000000005f798a002a0a000000000000c2898a002b0a000000000000299a8a002c0a00000000000091aa8a002d0a000000000000f9ba8a002e0a00000000000061cb8a002f0a000000000000c0d48a00300a00000000000070d98a00310a000000000000c8db8a00320a0000000000002eec8a00330a00000000000086ee8a00340a00000000000096fc8a00350a000000000000fe0c8b00360a000000000000661d8b00370a000000000000ce2d8b00380a000000000000de3b8b00390a000000000000363e8b003a0a0000000000009e4e8b003b0a000000000000065f8b003c0a0000000000006e6f8b003d0a000000000000d47f8b003e0a0000000000002c828b003f0a0000000000003c908b00400a0000000000009da08b00410a00000000000005b18b00420a0000000000006dc18b00430a000000000000d5d18b00440a0000000000003ce28b00450a000000000000a4f28b00460a0000000000000c038c00470a00000000000074138c00480a000000000000dc238c00490a00000000000044348c004a0a000000000000ac448c004b0a00000000000014558c004c0a0000000000007b658c004d0a000000000000e1758c004e0a00000000000049868c004f0a000000000000b1968c00500a00000000000018a78c00510a00000000000080b78c00520a000000000000e8c78c00530a00000000000050d88c00540a000000000000b8e88c00550a0000000000001ff98c00560a00000000000087098d00570a000000000000370e8d00580a000000000000e7128d00590a0000000000003f158d005a0a000000000000ef198d005b0a000000000000572a8d005c0a000000000000bf3a8d005d0a000000000000274b8d005e0a0000000000008f5b8d005f0a000000000000f76b8d00600a0000000000005f7c8d00610a000000000000c78c8d00620a0000000000002e9d8d00630a00000000000096ad8d00640a000000000000febd8d00650a00000000000066ce8d00660a000000000000cede8d00670a00000000000036ef8d00680a0000000000009eff8d00690a00000000000006108e006a0a0000000000006e208e006b0a000000000000d6308e006c0a0000000000003e418e006d0a000000000000a6518e006e0a0000000000000e628e006f0a00000000000076728e00700a000000000000de828e00710a00000000000046938e00720a000000000000aea38e00730a00000000000016b48e00740a0000000000007ec48e00750a000000000000e6d48e00760a0000000000004de58e00770a000000000000b3f58e00780a0000000000001b068f00790a00000000000083168f007a0a000000000000eb268f007b0a00000000000052378f007c0a000000000000ba478f007d0a00000000000022588f007e0a00000000000089688f007f0a000000000000ef788f00800a00000000000057898f00810a000000000000bf998f00820a00000000000077a58f00830a00000000000027aa8f00840a0000000000008fba8f00850a000000000000f7ca8f00860a0000000000005fdb8f00870a000000000000c7eb8f00880a0000000000002ffc8f00890a000000000000970c90008a0a000000000000ff1c90008b0a000000000000662d90008c0a000000000000ce3d90008d0a000000000000364e90008e0a000000000000935e90008f0a000000000000fb6e9000900a000000000000637f9000910a000000000000c98f9000920a0000000000002ca09000930a00000000000093b09000940a000000000000fac09000950a00000000000061d19000960a000000000000c8e19000970a0000000000002ff29000980a000000000000ed049100990a000000000000531591009a0a000000000000b12591009b0a000000000000173691009c0a000000000000764691009d0a000000000000db5691009e0a000000000000436791009f0a000000000000ab779100a00a0000000000000d889100a10a00000000000075989100a20a000000000000daa89100a30a00000000000042b99100a40a000000000000aac99100a50a00000000000012da9100a60a0000000000007aea9100a70a000000000000e1fa9100a80a000000000000490b9200a90a000000000000b11b9200aa0a000000000000192c9200ab0a000000000000813c9200ac0a000000000000e94c9200ad0a000000000000a95f9200ae0a00000000000011709200af0a00000000000079809200b00a000000000000d8909200b10a0000000000003fa19200b20a000000000000a7b19200b30a0000000000000fc29200b40a00000000000077d29200b50a000000000000dfe29200b60a00000000000047f39200b70a000000000000af039300b80a00000000000017149300b90a0000000000007f249300ba0a000000000000e6349300bb0a0000000000004d459300bc0a000000000000b5559300bd0a0000000000001d669300be0a00000000000085769300bf0a000000000000ed869300c00a00000000000055979300c10a000000000000baa79300c20a00000000000022b89300c30a0000000000008ac89300c40a000000000000f2d89300c50a0000000000005ae99300c60a000000000000c2f99300c70a0000000000002a0a9400c80a000000000000921a9400c90a000000000000f92a9400ca0a000000000000613b9400cb0a000000000000c74b9400cc0a0000000000002f5c9400cd0a000000000000976c9400ce0a000000000000fe7c9400cf0a000000000000668d9400d00a000000000000ce9d9400d10a00000000000032ae9400d20a0000000000009abe9400d30a000000000000ffce9400d40a00000000000067df9400d50a000000000000cfef9400d60a00000000000031009500d70a00000000000081109500d80a000000000000d1209500d90a000000000000f1309500da0a0000000000002f419500db0a00000000000068519500dc0a000000000000a2619500dd0a000000000000dc719500de0a00000000000003829500df0a0000000000003a929500e00a000000000000c4a49500e10a000000000000ffb49500e20a0000000000002ec59500e30a000000000000bed79500e40a00000000000009e89500e50a00000000000071f89500e60a000000000000d4089600e70a0000000000003c199600e80a000000000000a0299600e90a000000000000013a9600ea0a000000000000674a9600eb0a000000000000cd5a9600ec0a0000000000007d5f9600ed0a000000000000336b9600ee0a000000000000977b9600ef0a000000000000fc8b9600f00a0000000000005d9c9600f10a000000000000bfac9600f20a00000000000012bd9600f30a000000000000adcf9600f40a000000000000f7df9600f50a00000000000054f09600f60a00000000000001039700f70a0000000000005b139700f80a000000000000c1239700f90a00000000000068369700fa0a00000000000022499700fb0a000000000000d05b9700fc0a000000000000896e9700fd0a00000000000043819700fe0a000000000000a6919700ff0a000000000000b59f9700000b000000000000c3ad9700010b000000000000cfbb9700020b000000000000d7c99700030b000000000000dbd09700040b00000000000033d39700050b000000000000e3d79700060b0000000000003de19700070b000000000000ebe59700080b000000000000f2f39700090b000000000000fd0198000a0b000000000000031098000b0b000000000000051e98000c0b0000000000000c2c98000d0b000000000000103a98000e0b000000000000134898000f0b0000000000000d569800100b00000000000017649800110b00000000000014729800120b0000000000001a809800130b000000000000248e9800140b000000000000309c9800150b00000000000036aa9800160b00000000000041b89800170b0000000000004ac69800180b0000000000004fd49800190b00000000000058e298001a0b0000000000005ff098001b0b00000000000067fe98001c0b0000000000006b0c99001d0b0000000000006f1a99001e0b000000000000772899001f0b0000000000007a369900200b0000000000007c449900210b00000000000078529900220b00000000000074609900230b000000000000756e9900240b000000000000767c9900250b000000000000868a9900260b00000000000095989900270b000000000000a3a69900280b000000000000b1b49900290b000000000000bfc299002a0b000000000000ced099002b0b000000000000ddde99002c0b000000000000ecec99002d0b000000000000fcfa99002e0b0000000000000b099a002f0b0000000000001a179a00300b00000000000027259a00310b0000000000002d339a00320b0000000000002a419a00330b000000000000304f9a00340b0000000000002a5d9a00350b000000000000266b9a00360b0000000000001e799a00370b00000000000013879a00380b0000000000000c959a00390b00000000000004a39a003a0b000000000000feb09a003b0b000000000000f3be9a003c0b000000000000efcc9a003d0b000000000000e4da9a003e0b000000000000cbe89a003f0b000000000000b9f69a00400b000000000000ac049b00410b000000000000a1129b00420b00000000000091209b00430b0000000000007d2e9b00440b000000000000733c9b00450b0000000000006c4a9b00460b00000000000060589b00470b0000000000004b669b00480b00000000000036749b00490b00000000000023829b004a0b00000000000025909b004b0b0000000000000c9e9b004c0b00000000000007ac9b004d0b000000000000eeb99b004e0b000000000000d3c79b004f0b000000000000bad59b00500b0000000000009ce39b00510b0000000000007df19b00520b0000000000006cff9b00530b000000000000550d9c00540b000000000000481b9c00550b0000000000004b299c00560b00000000000022379c00570b0000000000000d459c00580b000000000000de529c00590b000000000000c1609c005a0b000000000000af6e9c005b0b0000000000009b7c9c005c0b0000000000007e8a9c005d0b00000000000062989c005e0b00000000000039a69c005f0b00000000000012b49c00600b000000000000f2c19c00610b000000000000dccf9c00620b000000000000b0dd9c00630b0000000000008ceb9c00640b00000000000075f99c00650b00000000000074079d00660b0000000000007d159d00670b0000000000008a239d00680b00000000000089319d00690b000000000000663f9d006a0b0000000000004b4d9d006b0b0000000000002f5b9d006c0b00000000000011699d006d0b000000000000fc769d006e0b000000000000e6849d006f0b000000000000c1929d00700b000000000000b7a09d00710b00000000000099ae9d00720b00000000000093bc9d00730b0000000000008dca9d00740b00000000000088d89d00750b00000000000072e69d00760b00000000000073f49d00770b00000000000073029e00780b0000000000006c109e00790b000000000000761e9e007a0b000000000000852c9e007b0b000000000000953a9e007c0b000000000000a5489e007d0b000000000000b4569e007e0b000000000000ba649e007f0b00000000000012679e00800b000000000000ca729e00810b000000000000d9809e00820b000000000000e98e9e00830b000000000000f89c9e00840b00000000000008ab9e00850b00000000000018b99e00860b00000000000028c79e00870b00000000000038d59e00880b000000000000e8d99e00890b00000000000048e39e008a0b00000000000058f19e008b0b00000000000068ff9e008c0b000000000000780d9f008d0b000000000000881b9f008e0b00000000000098299f008f0b000000000000a6379f00900b00000000000003439f00910b0000000000000b519f00920b0000000000001b5f9f00930b0000000000002b6d9f00940b0000000000003b7b9f00950b0000000000004b899f00960b0000000000005b979f00970b0000000000006aa59f00980b000000000000caae9f00990b0000000000007ab39f009a0b0000000000008ac19f009b0b0000000000009acf9f009c0b000000000000aadd9f009d0b000000000000baeb9f009e0b000000000000caf99f009f0b000000000000da07a000a00b000000000000ea15a000a10b000000000000fa23a000a20b0000000000000a32a000a30b0000000000001a40a000a40b000000000000294ea000a50b000000000000375ca000a60b000000000000476aa000a70b0000000000005778a000a80b0000000000006786a000a90b0000000000006294a000aa0b0000000000005ea2a000ab0b00000000000060b0a000ac0b00000000000063bea000ad0b00000000000073cca000ae0b00000000000083daa000af0b00000000000093e8a000b00b000000000000a3f6a000b10b000000000000b004a100b20b0000000000008f12a100b30b0000000000004a20a100b40b0000000000004b2ea100b50b0000000000005a3ca100b60b0000000000006a4aa100b70b0000000000006458a100b80b0000000000006d66a100b90b0000000000007d74a100ba0b0000000000008d82a100bb0b0000000000009d90a100bc0b0000000000009e9ea100bd0b000000000000aeaca100be0b000000000000bebaa100bf0b000000000000cec8a100c00b000000000000ddd6a100c10b000000000000ede4a100c20b000000000000fdf2a100c30b0000000000000d01a200c40b0000000000001c0fa200c50b0000000000007c18a200c60b000000000000241da200c70b000000000000d128a200c80b000000000000272ba200c90b0000000000001f32a200ca0b0000000000001e39a200cb0b0000000000001d47a200cc0b000000000000c84ba200cd0b0000000000001c55a200ce0b0000000000002063a200cf0b0000000000001f71a200d00b0000000000001c7fa200d10b000000000000208da200d20b0000000000001e9ba200d30b00000000000024a9a200d40b00000000000028b7a200d50b00000000000023c5a200d60b0000000000002fd3a200d70b0000000000003fe1a200d80b0000000000004fefa200d90b0000000000005ffda200da0b0000000000006f0ba300db0b0000000000007e19a300dc0b0000000000008e27a300dd0b0000000000009e35a300de0b000000000000ae43a300df0b000000000000bd51a300e00b000000000000cd5fa300e10b000000000000dd6da300e20b000000000000ed7ba300e30b000000000000fd89a300e40b0000000000000d98a300e50b0000000000001ca6a300e60b000000000000d4b1a300e70b0000000000002cb4a300e80b0000000000003cc2a300"}} diff --git a/rust/crates/truapi-server/src/smoldot_provider/wasm_helpers.rs b/rust/crates/truapi-server/src/smoldot_provider/wasm_helpers.rs new file mode 100644 index 00000000..a3895e7d --- /dev/null +++ b/rust/crates/truapi-server/src/smoldot_provider/wasm_helpers.rs @@ -0,0 +1,43 @@ +// Vendored from subxt-lightclient 0.50.0 (Apache-2.0 OR GPL-3.0) + +use super::wasm_socket::WasmSocket; + +use core::time::Duration; +use futures_util::{FutureExt, future}; + +/// Returns the current wall-clock time as a duration since the Unix epoch. +pub fn now_from_unix_epoch() -> Duration { + web_time::SystemTime::now() + .duration_since(web_time::SystemTime::UNIX_EPOCH) + .unwrap_or_else(|_| { + panic!("Invalid systime cannot be configured earlier than `UNIX_EPOCH`") + }) +} + +/// Monotonic instant type used by the wasm smoldot platform. +pub type Instant = web_time::Instant; + +/// Returns the current monotonic instant used by the wasm smoldot platform. +pub fn now() -> Instant { + web_time::Instant::now() +} + +/// Boxed delay future returned by `sleep`. +pub type Delay = future::BoxFuture<'static, ()>; + +/// Creates a future that resolves after the provided duration. +pub fn sleep(duration: Duration) -> Delay { + futures_timer::Delay::new(duration).boxed() +} + +/// Smoldot expects a single concrete stream type with pinned access; the +/// wrapper hides the buffer/socket pair behind a `pin_project` projection. +#[pin_project::pin_project] +pub struct Stream( + #[pin] + pub smoldot::libp2p::with_buffers::WithBuffers< + future::BoxFuture<'static, Result>, + WasmSocket, + Instant, + >, +); diff --git a/rust/crates/truapi-server/src/smoldot_provider/wasm_platform.rs b/rust/crates/truapi-server/src/smoldot_provider/wasm_platform.rs new file mode 100644 index 00000000..9a46bd03 --- /dev/null +++ b/rust/crates/truapi-server/src/smoldot_provider/wasm_platform.rs @@ -0,0 +1,218 @@ +// Vendored from subxt-lightclient 0.50.0 (Apache-2.0 OR GPL-3.0) + +use super::wasm_socket::WasmSocket; + +use core::{ + fmt::{self, Write as _}, + net::IpAddr, + time::Duration, +}; +use futures::prelude::*; +use smoldot::libp2p::with_buffers; +use smoldot_light::platform::{ + Address, ConnectionType, LogLevel, MultiStreamAddress, MultiStreamWebRtcConnection, + PlatformRef, SubstreamDirection, +}; +use wasm_bindgen::JsValue; + +use std::{io, net::SocketAddr, pin::Pin}; + +/// Alias for the platform reference type smoldot-light expects. +pub type PlatformRefAlias = TrUApiWasmPlatform; + +/// Creates the wasm-backed smoldot platform implementation used by the server. +pub fn make_platform() -> PlatformRefAlias { + TrUApiWasmPlatform::new() +} + +/// Smoldot platform implementation backed by the browser WebSocket API and +/// `wasm_bindgen_futures` for task spawning. +#[derive(Clone)] +pub struct TrUApiWasmPlatform {} + +impl TrUApiWasmPlatform { + /// Builds a fresh wasm platform handle. The handle is cheaply cloneable. + pub fn new() -> Self { + TrUApiWasmPlatform {} + } +} + +impl Default for TrUApiWasmPlatform { + fn default() -> Self { + Self::new() + } +} + +impl PlatformRef for TrUApiWasmPlatform { + type Delay = super::wasm_helpers::Delay; + type Instant = super::wasm_helpers::Instant; + type MultiStream = std::convert::Infallible; + type Stream = super::wasm_helpers::Stream; + type StreamConnectFuture = future::Ready; + type MultiStreamConnectFuture = future::Pending>; + type ReadWriteAccess<'a> = with_buffers::ReadWriteAccess<'a, Self::Instant>; + type StreamUpdateFuture<'a> = future::BoxFuture<'a, ()>; + type StreamErrorRef<'a> = &'a std::io::Error; + type NextSubstreamFuture<'a> = future::Pending>; + + fn now_from_unix_epoch(&self) -> Duration { + super::wasm_helpers::now_from_unix_epoch() + } + + fn now(&self) -> Self::Instant { + super::wasm_helpers::now() + } + + fn fill_random_bytes(&self, buffer: &mut [u8]) { + getrandom::getrandom(buffer).expect("Cannot fill random bytes"); + } + + fn sleep(&self, duration: Duration) -> Self::Delay { + super::wasm_helpers::sleep(duration) + } + + fn sleep_until(&self, when: Self::Instant) -> Self::Delay { + self.sleep(when.saturating_duration_since(self.now())) + } + + fn spawn_task( + &self, + _task_name: std::borrow::Cow<'_, str>, + task: impl future::Future + Send + 'static, + ) { + wasm_bindgen_futures::spawn_local(task); + } + + fn client_name(&self) -> std::borrow::Cow<'_, str> { + "truapi".into() + } + + fn client_version(&self) -> std::borrow::Cow<'_, str> { + env!("CARGO_PKG_VERSION").into() + } + + fn supports_connection_type(&self, connection_type: ConnectionType) -> bool { + matches!( + connection_type, + ConnectionType::WebSocketIpv4 { .. } + | ConnectionType::WebSocketIpv6 { .. } + | ConnectionType::WebSocketDns { .. } + ) + } + + fn connect_stream(&self, multiaddr: Address) -> Self::StreamConnectFuture { + let addr = match multiaddr { + Address::WebSocketDns { + hostname, + port, + secure: true, + } => { + format!("wss://{hostname}:{port}") + } + Address::WebSocketDns { + hostname, + port, + secure: false, + } => { + format!("ws://{hostname}:{port}") + } + Address::WebSocketIp { + ip: IpAddr::V4(ip), + port, + } => { + let addr = SocketAddr::from((ip, port)); + format!("ws://{addr}") + } + Address::WebSocketIp { + ip: IpAddr::V6(ip), + port, + } => { + let addr = SocketAddr::from((ip, port)); + format!("ws://{addr}") + } + _ => { + unreachable!("Unsupported connection type") + } + }; + + let socket_future = async move { + WasmSocket::new(addr.as_str()).map_err(|err| std::io::Error::other(err.to_string())) + }; + + future::ready(super::wasm_helpers::Stream(with_buffers::WithBuffers::new( + Box::pin(socket_future), + ))) + } + + fn connect_multistream(&self, _address: MultiStreamAddress) -> Self::MultiStreamConnectFuture { + panic!("Multistreams are not supported") + } + + fn open_out_substream(&self, c: &mut Self::MultiStream) { + match *c {} + } + + fn next_substream(&self, c: &'_ mut Self::MultiStream) -> Self::NextSubstreamFuture<'_> { + match *c {} + } + + fn read_write_access<'a>( + &self, + stream: Pin<&'a mut Self::Stream>, + ) -> Result, &'a io::Error> { + let stream = stream.project(); + stream.0.read_write_access(Self::Instant::now()) + } + + fn wait_read_write_again<'a>( + &self, + stream: Pin<&'a mut Self::Stream>, + ) -> Self::StreamUpdateFuture<'a> { + let stream = stream.project(); + Box::pin(stream.0.wait_read_write_again(|when| async move { + let now = super::wasm_helpers::now(); + let duration = when.saturating_duration_since(now); + super::wasm_helpers::sleep(duration).await; + })) + } + + fn log<'a>( + &self, + log_level: LogLevel, + log_target: &'a str, + message: &'a str, + key_values: impl Iterator, + ) { + // Smoldot is extremely chatty at debug/trace level (per-connection + // activity, gossip events, sync-service progress). Even with + // console.debug, which is hidden in Chrome's default verbosity, + // the volume costs measurable CPU on string formatting and shows + // up under Verbose. Suppress debug+trace entirely; warn/error/info + // still pass through so real problems surface. + if matches!(log_level, LogLevel::Debug | LogLevel::Trace) { + return; + } + + let mut message_build = String::with_capacity(128); + message_build.push_str(message); + let mut first = true; + for (key, value) in key_values { + if first { + let _ = write!(message_build, "; "); + first = false; + } else { + let _ = write!(message_build, ", "); + } + let _ = write!(message_build, "{key}={value}"); + } + + let formatted = format!("[{log_target}] {message_build}"); + let js = JsValue::from_str(&formatted); + match log_level { + LogLevel::Error => web_sys::console::error_1(&js), + LogLevel::Warn => web_sys::console::warn_1(&js), + LogLevel::Info => web_sys::console::info_1(&js), + LogLevel::Debug | LogLevel::Trace => unreachable!(), + } + } +} diff --git a/rust/crates/truapi-server/src/smoldot_provider/wasm_socket.rs b/rust/crates/truapi-server/src/smoldot_provider/wasm_socket.rs new file mode 100644 index 00000000..ae09c56c --- /dev/null +++ b/rust/crates/truapi-server/src/smoldot_provider/wasm_socket.rs @@ -0,0 +1,235 @@ +// Vendored from subxt-lightclient 0.50.0 (Apache-2.0 OR GPL-3.0) + +use futures::{io, prelude::*}; +use send_wrapper::SendWrapper; +use wasm_bindgen::{JsCast, prelude::*}; + +use std::{ + collections::VecDeque, + fmt, + pin::Pin, + sync::{Arc, Mutex}, + task::Poll, + task::{Context, Waker}, +}; + +/// Errors returned by the wasm-side socket constructor. +#[derive(Debug)] +pub enum Error { + /// Wraps a JS-side connection failure. + ConnectionError(String), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ConnectionError(msg) => write!(f, "Failed to connect {msg}"), + } + } +} + +impl std::error::Error for Error {} + +/// WebSocket for WASM environments. Not safe to clone because the inner +/// `web_sys::WebSocket` is not thread-safe and the wakers borrow the same +/// `Mutex`. +pub struct WasmSocket { + inner: Arc>, + socket: SendWrapper, + _callbacks: SendWrapper, +} + +#[derive(PartialEq, Eq, Clone, Copy)] +enum ConnectionState { + Connecting, + Opened, + Closed, + Error, +} + +struct InnerWasmSocket { + state: ConnectionState, + data: VecDeque, + waker: Option, +} + +type Callbacks = ( + Closure, + Closure, + Closure, + Closure, +); + +impl WasmSocket { + /// Opens a new WebSocket connection to the given URL. Returns an error + /// if the browser refuses to construct the underlying `WebSocket` + /// object. + pub fn new(addr: &str) -> Result { + let socket = match web_sys::WebSocket::new(addr) { + Ok(socket) => socket, + Err(err) => return Err(Error::ConnectionError(format!("{err:?}"))), + }; + + socket.set_binary_type(web_sys::BinaryType::Arraybuffer); + + let inner = Arc::new(Mutex::new(InnerWasmSocket { + state: ConnectionState::Connecting, + data: VecDeque::with_capacity(16384), + waker: None, + })); + + let open_callback = Closure::::new({ + let inner = inner.clone(); + move || { + let mut inner = inner.lock().expect("Mutex is poised; qed"); + inner.state = ConnectionState::Opened; + + if let Some(waker) = inner.waker.take() { + waker.wake(); + } + } + }); + socket.set_onopen(Some(open_callback.as_ref().unchecked_ref())); + + let message_callback = Closure::::new({ + let inner = inner.clone(); + move |event: web_sys::MessageEvent| { + let Ok(buffer) = event.data().dyn_into::() else { + panic!("Unexpected data format {:?}", event.data()); + }; + + let mut inner = inner.lock().expect("Mutex is poised; qed"); + let bytes = js_sys::Uint8Array::new(&buffer).to_vec(); + inner.data.extend(bytes); + + if let Some(waker) = inner.waker.take() { + waker.wake(); + } + } + }); + socket.set_onmessage(Some(message_callback.as_ref().unchecked_ref())); + + let error_callback = Closure::::new({ + let inner = inner.clone(); + move |_event: web_sys::Event| { + let mut inner = inner.lock().expect("Mutex is poised; qed"); + inner.state = ConnectionState::Error; + + if let Some(waker) = inner.waker.take() { + waker.wake(); + } + } + }); + socket.set_onerror(Some(error_callback.as_ref().unchecked_ref())); + + let close_callback = Closure::::new({ + let inner = inner.clone(); + move |_event: web_sys::CloseEvent| { + let mut inner = inner.lock().expect("Mutex is poised; qed"); + inner.state = ConnectionState::Closed; + + if let Some(waker) = inner.waker.take() { + waker.wake(); + } + } + }); + socket.set_onclose(Some(close_callback.as_ref().unchecked_ref())); + + let callbacks = ( + open_callback, + message_callback, + error_callback, + close_callback, + ); + + Ok(Self { + inner, + socket: SendWrapper::new(socket), + _callbacks: SendWrapper::new(callbacks), + }) + } +} + +impl AsyncRead for WasmSocket { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + let mut inner = self.inner.lock().expect("Mutex is poised; qed"); + inner.waker = Some(cx.waker().clone()); + + if self.socket.ready_state() == web_sys::WebSocket::CONNECTING { + return Poll::Pending; + } + + match inner.state { + ConnectionState::Error => Poll::Ready(Err(io::Error::other("Socket error"))), + ConnectionState::Closed => Poll::Ready(Err(io::ErrorKind::BrokenPipe.into())), + ConnectionState::Connecting => Poll::Pending, + ConnectionState::Opened => { + if inner.data.is_empty() { + return Poll::Pending; + } + + let n = inner.data.len().min(buf.len()); + for k in buf.iter_mut().take(n) { + *k = inner.data.pop_front().expect("Buffer non empty; qed"); + } + Poll::Ready(Ok(n)) + } + } + } +} + +impl AsyncWrite for WasmSocket { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let mut inner = self.inner.lock().expect("Mutex is poised; qed"); + inner.waker = Some(cx.waker().clone()); + + match inner.state { + ConnectionState::Error => Poll::Ready(Err(io::Error::other("Socket error"))), + ConnectionState::Closed => Poll::Ready(Err(io::ErrorKind::BrokenPipe.into())), + ConnectionState::Connecting => Poll::Pending, + ConnectionState::Opened => match self.socket.send_with_u8_array(buf) { + Ok(()) => Poll::Ready(Ok(buf.len())), + Err(err) => Poll::Ready(Err(io::Error::other(format!("Write error: {err:?}")))), + }, + } + } + + fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + if self.socket.ready_state() == web_sys::WebSocket::CLOSED { + return Poll::Ready(Ok(())); + } + + if self.socket.ready_state() != web_sys::WebSocket::CLOSING { + let _ = self.socket.close(); + } + + let mut inner = self.inner.lock().expect("Mutex is poised; qed"); + inner.waker = Some(cx.waker().clone()); + Poll::Pending + } +} + +impl Drop for WasmSocket { + fn drop(&mut self) { + if self.socket.ready_state() != web_sys::WebSocket::CLOSING { + let _ = self.socket.close(); + } + + self.socket.set_onopen(None); + self.socket.set_onmessage(None); + self.socket.set_onerror(None); + self.socket.set_onclose(None); + } +} From f70b432b3b00a594c3281ebc246c1538183eea48 Mon Sep 17 00:00:00 2001 From: pgherveou Date: Sun, 17 May 2026 09:47:49 +0000 Subject: [PATCH 06/49] feat(host-libs): @parity/host-{shared,web,electron} packages (Phase 6 JS, #103) Three new npm packages under host-libs/js/ wrapping the truapi-server WASM core for browser, worker, and electron host contexts. @parity/host-shared: - WasmRawCallbacks matching Rust JsBridge::from_js (navigateTo, devicePermission, remotePermission, featureSupported, getLegacyAccounts, ...) - createWasmProvider / createNodeWasmProvider (lazy WASM load) - Web Worker entrypoint + worker-protocol wire shape - Re-exports createHostServer / toResponsePayload / toFlatResponsePayload from @parity/truapi-host @parity/host-web: - createIframeHost: embeds an iframe, handshakes a MessagePort via the `truapi-init` message, surfaces the host-side port via onPort. - createWebWorkerProvider: bridges a Web Worker to a Provider. - No legacy @novasamatech/host-api compat path. @parity/host-electron: - createElectronProvider({port}): wraps an Electron MessagePortMain as a Provider. Pre-built WASM artifacts committed under host-libs/js/shared/dist/wasm/ (web + node targets, ~924 KB each, built with `wasm-pack --no-default-features`, smoldot feature off in the shipped bundle). Tests: 5 (shared) + 2 (web) + 2 (electron) = 9 passing via `node --test`. tsc --noEmit clean across all three packages. No changes to truapi/src/api/*. Relates to #96, #103. --- host-libs/js/electron/.gitignore | 3 + host-libs/js/electron/README.md | 29 + host-libs/js/electron/package-lock.json | 66 ++ host-libs/js/electron/package.json | 32 + host-libs/js/electron/src/index.ts | 92 +++ .../js/electron/test/preload-bridge.test.mjs | 90 +++ host-libs/js/electron/tsconfig.json | 15 + host-libs/js/shared/.gitignore | 8 + host-libs/js/shared/README.md | 80 +++ host-libs/js/shared/dist/wasm/node/README.md | 31 + .../js/shared/dist/wasm/node/package.json | 13 + .../shared/dist/wasm/node/truapi_server.d.ts | 45 ++ .../js/shared/dist/wasm/node/truapi_server.js | 534 +++++++++++++++ .../dist/wasm/node/truapi_server_bg.wasm | Bin 0 -> 943201 bytes .../dist/wasm/node/truapi_server_bg.wasm.d.ts | 79 +++ host-libs/js/shared/dist/wasm/web/README.md | 31 + .../js/shared/dist/wasm/web/package.json | 17 + .../shared/dist/wasm/web/truapi_server.d.ts | 149 +++++ .../js/shared/dist/wasm/web/truapi_server.js | 627 ++++++++++++++++++ .../dist/wasm/web/truapi_server_bg.wasm | Bin 0 -> 943201 bytes .../dist/wasm/web/truapi_server_bg.wasm.d.ts | 79 +++ host-libs/js/shared/package-lock.json | 67 ++ host-libs/js/shared/package.json | 47 ++ host-libs/js/shared/scripts/build-wasm.mjs | 42 ++ host-libs/js/shared/src/dispatcher.ts | 20 + host-libs/js/shared/src/index.ts | 41 ++ host-libs/js/shared/src/node-runtime.ts | 44 ++ host-libs/js/shared/src/runtime.ts | 191 ++++++ host-libs/js/shared/src/types.ts | 9 + host-libs/js/shared/src/worker-protocol.ts | 76 +++ host-libs/js/shared/src/worker-runtime.ts | 259 ++++++++ .../shared/test/dispatcher-roundtrip.test.mjs | 79 +++ .../shared/test/node-wasm-provider.test.mjs | 59 ++ .../js/shared/test/worker-protocol.test.mjs | 30 + host-libs/js/shared/tsconfig.json | 15 + host-libs/js/web/.gitignore | 3 + host-libs/js/web/README.md | 49 ++ host-libs/js/web/package-lock.json | 66 ++ host-libs/js/web/package.json | 32 + host-libs/js/web/src/create-iframe-host.ts | 140 ++++ .../js/web/src/create-worker-host-runtime.ts | 370 +++++++++++ host-libs/js/web/src/index.ts | 4 + .../js/web/test/create-iframe-host.test.mjs | 115 ++++ host-libs/js/web/tsconfig.json | 15 + 44 files changed, 3793 insertions(+) create mode 100644 host-libs/js/electron/.gitignore create mode 100644 host-libs/js/electron/README.md create mode 100644 host-libs/js/electron/package-lock.json create mode 100644 host-libs/js/electron/package.json create mode 100644 host-libs/js/electron/src/index.ts create mode 100644 host-libs/js/electron/test/preload-bridge.test.mjs create mode 100644 host-libs/js/electron/tsconfig.json create mode 100644 host-libs/js/shared/.gitignore create mode 100644 host-libs/js/shared/README.md create mode 100644 host-libs/js/shared/dist/wasm/node/README.md create mode 100644 host-libs/js/shared/dist/wasm/node/package.json create mode 100644 host-libs/js/shared/dist/wasm/node/truapi_server.d.ts create mode 100644 host-libs/js/shared/dist/wasm/node/truapi_server.js create mode 100644 host-libs/js/shared/dist/wasm/node/truapi_server_bg.wasm create mode 100644 host-libs/js/shared/dist/wasm/node/truapi_server_bg.wasm.d.ts create mode 100644 host-libs/js/shared/dist/wasm/web/README.md create mode 100644 host-libs/js/shared/dist/wasm/web/package.json create mode 100644 host-libs/js/shared/dist/wasm/web/truapi_server.d.ts create mode 100644 host-libs/js/shared/dist/wasm/web/truapi_server.js create mode 100644 host-libs/js/shared/dist/wasm/web/truapi_server_bg.wasm create mode 100644 host-libs/js/shared/dist/wasm/web/truapi_server_bg.wasm.d.ts create mode 100644 host-libs/js/shared/package-lock.json create mode 100644 host-libs/js/shared/package.json create mode 100644 host-libs/js/shared/scripts/build-wasm.mjs create mode 100644 host-libs/js/shared/src/dispatcher.ts create mode 100644 host-libs/js/shared/src/index.ts create mode 100644 host-libs/js/shared/src/node-runtime.ts create mode 100644 host-libs/js/shared/src/runtime.ts create mode 100644 host-libs/js/shared/src/types.ts create mode 100644 host-libs/js/shared/src/worker-protocol.ts create mode 100644 host-libs/js/shared/src/worker-runtime.ts create mode 100644 host-libs/js/shared/test/dispatcher-roundtrip.test.mjs create mode 100644 host-libs/js/shared/test/node-wasm-provider.test.mjs create mode 100644 host-libs/js/shared/test/worker-protocol.test.mjs create mode 100644 host-libs/js/shared/tsconfig.json create mode 100644 host-libs/js/web/.gitignore create mode 100644 host-libs/js/web/README.md create mode 100644 host-libs/js/web/package-lock.json create mode 100644 host-libs/js/web/package.json create mode 100644 host-libs/js/web/src/create-iframe-host.ts create mode 100644 host-libs/js/web/src/create-worker-host-runtime.ts create mode 100644 host-libs/js/web/src/index.ts create mode 100644 host-libs/js/web/test/create-iframe-host.test.mjs create mode 100644 host-libs/js/web/tsconfig.json diff --git a/host-libs/js/electron/.gitignore b/host-libs/js/electron/.gitignore new file mode 100644 index 00000000..f4e2c6d6 --- /dev/null +++ b/host-libs/js/electron/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.tsbuildinfo diff --git a/host-libs/js/electron/README.md b/host-libs/js/electron/README.md new file mode 100644 index 00000000..7f70a534 --- /dev/null +++ b/host-libs/js/electron/README.md @@ -0,0 +1,29 @@ +# @parity/host-electron + +Electron TrUAPI host wrapper. Exposes `createElectronProvider`, which +wraps an Electron `MessagePortMain` as a `Provider` from +`@parity/truapi`. Pair it with `createNodeWasmProvider` from +`@parity/host-shared` and `createHostServer` from `@parity/truapi-host` +to assemble a full Electron host. + +## Architecture + +1. preload script transfers `port2` into renderer +2. main process keeps `port1` +3. `createElectronProvider({ port: port1 })` returns a `Provider` +4. host code feeds that provider into `createHostServer` + +## Example + +```ts +import { createNodeWasmProvider } from "@parity/host-shared"; +import { createHostServer } from "@parity/truapi-host"; +import { createElectronProvider } from "@parity/host-electron"; + +const coreProvider = await createNodeWasmProvider(callbacks); +const rendererProvider = createElectronProvider({ port: messagePortMain }); + +const server = createHostServer(rendererProvider, [ + /* dispatch entries */ +]); +``` diff --git a/host-libs/js/electron/package-lock.json b/host-libs/js/electron/package-lock.json new file mode 100644 index 00000000..e385f1b4 --- /dev/null +++ b/host-libs/js/electron/package-lock.json @@ -0,0 +1,66 @@ +{ + "name": "@parity/host-electron", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@parity/host-electron", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@parity/host-shared": "file:../shared", + "@parity/truapi": "file:../../../js/packages/truapi" + }, + "devDependencies": { + "typescript": "^5.7" + } + }, + "../../../js/packages/truapi": { + "name": "@parity/truapi", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "neverthrow": "^8.2.0", + "scale-ts": "^1.6.1" + }, + "devDependencies": { + "typescript": "^5.7" + } + }, + "../shared": { + "name": "@parity/host-shared", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@parity/truapi": "file:../../../js/packages/truapi", + "@parity/truapi-host": "file:../../../js/packages/truapi-host" + }, + "devDependencies": { + "typescript": "^5.7" + } + }, + "node_modules/@parity/host-shared": { + "resolved": "../shared", + "link": true + }, + "node_modules/@parity/truapi": { + "resolved": "../../../js/packages/truapi", + "link": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/host-libs/js/electron/package.json b/host-libs/js/electron/package.json new file mode 100644 index 00000000..fb590d16 --- /dev/null +++ b/host-libs/js/electron/package.json @@ -0,0 +1,32 @@ +{ + "name": "@parity/host-electron", + "version": "0.1.0", + "description": "Electron TrUAPI host wrapper: MessagePortMain bridge", + "license": "MIT", + "author": "Parity Technologies ", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "tsc", + "prepare": "npm run build", + "test": "for f in test/*.test.mjs; do echo \"=== $f ===\" && node --test \"$f\" || exit 1; done" + }, + "dependencies": { + "@parity/host-shared": "file:../shared", + "@parity/truapi": "file:../../../js/packages/truapi" + }, + "devDependencies": { + "typescript": "^5.7" + } +} diff --git a/host-libs/js/electron/src/index.ts b/host-libs/js/electron/src/index.ts new file mode 100644 index 00000000..fc2b18b2 --- /dev/null +++ b/host-libs/js/electron/src/index.ts @@ -0,0 +1,92 @@ +import type { Provider } from "@parity/truapi"; + +/** + * Minimal subset of Electron's `MessagePortMain` interface used by this + * package. Kept local so the package does not have a hard `electron` + * dependency (the host code passes the port in at runtime). + */ +export interface ElectronMessagePortMain { + postMessage(message: unknown, transfer?: unknown[]): void; + on(event: "message", handler: (event: { data: unknown }) => void): this; + on(event: "close", handler: () => void): this; + off(event: "message", handler: (event: { data: unknown }) => void): this; + off(event: "close", handler: () => void): this; + start(): void; + close(): void; +} + +/** + * Options for `createElectronProvider`. + */ +export interface CreateElectronProviderOptions { + /** One end of an Electron `MessageChannelMain`. The other end must be + * transferred to the renderer through the preload script. */ + port: ElectronMessagePortMain; +} + +/** + * Wrap an Electron `MessagePortMain` as a TrUAPI `Provider`. The + * provider exchanges SCALE-encoded `Uint8Array` frames with the renderer. + * The provider's `dispose` closes the port. + * + * Hosts typically pair this with `@parity/host-shared`'s + * `createNodeWasmProvider` (for the WASM core) and `createHostServer` + * from `@parity/truapi-host` (for the dispatcher) to assemble a full + * Electron host. + */ +export function createElectronProvider( + options: CreateElectronProviderOptions, +): Provider { + const { port } = options; + const listeners = new Set<(message: Uint8Array) => void>(); + const closeListeners = new Set<(error: Error) => void>(); + let disposed = false; + + const onMessage = (event: { data: unknown }): void => { + if (disposed) return; + const data = event.data; + if (!(data instanceof Uint8Array)) return; + for (const listener of [...listeners]) listener(data); + }; + + const onClose = (): void => { + const error = new Error("electron message port closed"); + for (const listener of [...closeListeners]) listener(error); + }; + + port.on("message", onMessage); + port.on("close", onClose); + port.start(); + + return { + postMessage(bytes: Uint8Array): void { + if (disposed) return; + port.postMessage(bytes); + }, + subscribe(callback) { + listeners.add(callback); + return () => { + listeners.delete(callback); + }; + }, + subscribeClose(callback) { + closeListeners.add(callback); + return () => { + closeListeners.delete(callback); + }; + }, + dispose() { + if (disposed) return; + disposed = true; + try { + port.off("message", onMessage); + port.off("close", onClose); + port.close(); + } catch { + // already closed + } + listeners.clear(); + closeListeners.clear(); + }, + }; +} diff --git a/host-libs/js/electron/test/preload-bridge.test.mjs b/host-libs/js/electron/test/preload-bridge.test.mjs new file mode 100644 index 00000000..2a707e41 --- /dev/null +++ b/host-libs/js/electron/test/preload-bridge.test.mjs @@ -0,0 +1,90 @@ +// Verify `createElectronProvider` adapts an Electron-style port into a +// TrUAPI Provider: subscribers receive inbound binary frames, outbound +// frames flow back through `port.postMessage`, and `dispose` closes the +// port and clears listeners. + +import assert from "node:assert/strict"; +import test from "node:test"; + +import { createElectronProvider } from "../dist/index.js"; + +function makeFakePort() { + const messageListeners = new Set(); + const closeListeners = new Set(); + const sent = []; + let closed = false; + return { + sent, + isClosed: () => closed, + deliverMessage(data) { + for (const listener of [...messageListeners]) listener({ data }); + }, + deliverClose() { + for (const listener of [...closeListeners]) listener(); + }, + api: { + postMessage(message) { + sent.push(message); + }, + on(event, handler) { + if (event === "message") messageListeners.add(handler); + else if (event === "close") closeListeners.add(handler); + return this; + }, + off(event, handler) { + if (event === "message") messageListeners.delete(handler); + else if (event === "close") closeListeners.delete(handler); + return this; + }, + start() {}, + close() { + closed = true; + }, + }, + }; +} + +test("createElectronProvider forwards inbound frames to subscribers and outbound frames to the port", () => { + const fake = makeFakePort(); + const provider = createElectronProvider({ port: fake.api }); + + const received = []; + const unsubscribe = provider.subscribe((message) => { + received.push(message); + }); + + const inbound = new Uint8Array([10, 20, 30]); + fake.deliverMessage(inbound); + // Non-binary frames are ignored. + fake.deliverMessage({ type: "ignored" }); + + assert.equal(received.length, 1); + assert.deepEqual(Array.from(received[0]), [10, 20, 30]); + + const outbound = new Uint8Array([1, 2]); + provider.postMessage(outbound); + assert.equal(fake.sent.length, 1); + assert.deepEqual(Array.from(fake.sent[0]), [1, 2]); + + unsubscribe(); + fake.deliverMessage(new Uint8Array([99])); + // Unsubscribed, length should not grow. + assert.equal(received.length, 1); + + provider.dispose(); + assert.equal(fake.isClosed(), true, "dispose closes the underlying port"); +}); + +test("createElectronProvider notifies close subscribers when the port closes", () => { + const fake = makeFakePort(); + const provider = createElectronProvider({ port: fake.api }); + + const closes = []; + provider.subscribeClose((error) => closes.push(error)); + + fake.deliverClose(); + assert.equal(closes.length, 1); + assert.ok(closes[0] instanceof Error); + + provider.dispose(); +}); diff --git a/host-libs/js/electron/tsconfig.json b/host-libs/js/electron/tsconfig.json new file mode 100644 index 00000000..033ca333 --- /dev/null +++ b/host-libs/js/electron/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "lib": ["ES2022", "DOM"] + }, + "include": ["src"] +} diff --git a/host-libs/js/shared/.gitignore b/host-libs/js/shared/.gitignore new file mode 100644 index 00000000..c23d2b37 --- /dev/null +++ b/host-libs/js/shared/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +*.tsbuildinfo +# Ignore compiled TS output but keep the committed WASM artifacts. +dist/*.js +dist/*.d.ts +dist/*.js.map +dist/*.d.ts.map +!dist/wasm/ diff --git a/host-libs/js/shared/README.md b/host-libs/js/shared/README.md new file mode 100644 index 00000000..5b641bd1 --- /dev/null +++ b/host-libs/js/shared/README.md @@ -0,0 +1,80 @@ +# @parity/host-shared + +Shared TrUAPI host runtime layer. Provides: + +- a WASM-backed `Provider` factory (`createNodeWasmProvider`, + `createWasmProvider`) compatible with `@parity/truapi` +- a Web Worker entrypoint at `dist/worker-runtime.js` that owns the + truapi-server WASM core off the page main thread +- generic dispatcher re-exports from `@parity/truapi-host` so hosts can + install a single shared package and get both the WASM bridge and the + typed handler dispatcher + +## Pre-built WASM artefacts + +The committed bundles under `dist/wasm/web/` and `dist/wasm/node/` are +built without smoldot (`wasm-pack build --no-default-features`). Hosts +that already manage chain access through their own JSON-RPC provider +wire `chainConnect` into the callbacks and never touch smoldot. The +bundled WASM is about 1 MB (release build with `wasm-opt`). + +To rebuild after editing `rust/crates/truapi-server`: + +```bash +npm run build:wasm +``` + +This rerun requires `wasm-pack` on PATH. + +## Example (Node/Electron) + +```ts +import { createNodeWasmProvider, createHostServer } from "@parity/host-shared"; + +const provider = await createNodeWasmProvider({ + navigateTo: async (url) => { + /* shell.openExternal(url) */ + }, + pushNotification: async () => {}, + devicePermission: async () => true, + remotePermission: async () => true, + featureSupported: async (payload) => payload, + localStorageRead: async () => undefined, + localStorageWrite: async () => {}, + localStorageClear: async () => {}, + accountGet: async () => new Uint8Array(), + accountGetAlias: async () => new Uint8Array(), + accountCreateProof: async () => new Uint8Array(), + getLegacyAccounts: async () => new Uint8Array(), + accountConnectionStatusSubscribe: () => {}, + getUserId: async () => new Uint8Array(), + signPayload: async () => new Uint8Array(), + signRaw: async () => new Uint8Array(), + statementStoreSubscribe: () => {}, + statementStoreSubmit: async () => new Uint8Array(), + statementStoreCreateProof: async () => new Uint8Array(), + preimageLookupSubscribe: () => {}, +}); + +const server = createHostServer(provider, [ + /* dispatch entries */ +]); +``` + +For web hosts see `@parity/host-web`'s `createWebWorkerProvider`. + +## Architecture + +```text +JS host code + protocol handlers / typed callbacks + | + v +createHostServer (from @parity/truapi-host) <-- bytes --> Provider + | + v + createWasmProvider / Worker + | + v + truapi-server WASM core +``` diff --git a/host-libs/js/shared/dist/wasm/node/README.md b/host-libs/js/shared/dist/wasm/node/README.md new file mode 100644 index 00000000..4f30f9a0 --- /dev/null +++ b/host-libs/js/shared/dist/wasm/node/README.md @@ -0,0 +1,31 @@ +# truapi-server + +*Runtime core for TrUAPI: dispatcher, protocol frames, SCALE-coded wire envelope.* + +## What this crate is for + +`truapi-server` is the runtime that turns trait implementations of the +`truapi` API into a working host. It owns: + +- the [`ProtocolMessage`] wire envelope and SCALE codec +- the [`Dispatcher`] that routes incoming frames to per-method handlers +- the subscription lifecycle (start/receive/stop/interrupt) +- the [`Transport`] trait that platform-specific IPC backends implement +- the auto-generated dispatcher/wire-table tables shipped under + [`crate::generated`] + +## Wire envelope + +Every frame on the wire is encoded as: + +```text +[requestId: SCALE str][discriminant: u8][payload bytes...] +``` + +The discriminant maps to a method/kind tag via the auto-generated +[`crate::generated::wire_table::WIRE_TABLE`]. Method ordering is part of +the wire protocol; only ever append. + +The payload bytes are the SCALE-encoded inner value, inlined without a +length prefix. In-memory we keep the tag as a `String` so the dispatcher +(which keys on method name) is independent of the wire numbering. diff --git a/host-libs/js/shared/dist/wasm/node/package.json b/host-libs/js/shared/dist/wasm/node/package.json new file mode 100644 index 00000000..3a37ba05 --- /dev/null +++ b/host-libs/js/shared/dist/wasm/node/package.json @@ -0,0 +1,13 @@ +{ + "name": "truapi-server", + "description": "TrUAPI server runtime: dispatcher, frames, SCALE, streams", + "version": "0.1.0", + "license": "MIT", + "files": [ + "truapi_server_bg.wasm", + "truapi_server.js", + "truapi_server.d.ts" + ], + "main": "truapi_server.js", + "types": "truapi_server.d.ts" +} \ No newline at end of file diff --git a/host-libs/js/shared/dist/wasm/node/truapi_server.d.ts b/host-libs/js/shared/dist/wasm/node/truapi_server.d.ts new file mode 100644 index 00000000..0281fb58 --- /dev/null +++ b/host-libs/js/shared/dist/wasm/node/truapi_server.d.ts @@ -0,0 +1,45 @@ +/* tslint:disable */ +/* eslint-disable */ + +/** + * JS-callable handle to the TrUAPI core. Constructed once per shell boot. + */ +export class WasmTrUApiCore { + free(): void; + [Symbol.dispose](): void; + /** + * Drop the currently-paired session. + */ + clearActiveSession(): void; + /** + * Tear down the bridge. Invokes the JS-side `dispose` callback so the + * host can drop its end of the wiring. + */ + dispose(): void; + /** + * Build the core from a JS callbacks object. The object must define + * every host capability the [`truapi_platform::Platform`] trait set + * requires (camelCase property names; see the source for the full + * list). + */ + constructor(callbacks: any); + /** + * Push a SCALE-encoded protocol frame into the dispatcher. Responses + * (and subscription items) flow back through the `emitFrame` + * callback. + */ + receiveFromProduct(frame: Uint8Array): Promise; + /** + * Push the currently-paired session into the core. Called by the + * host shell whenever the user pairs / unpairs. `pubkey` must be + * exactly 32 bytes (sr25519 root public key); usernames may be + * null / undefined when the identity record carries no value. + */ + setActiveSession(pubkey: Uint8Array, lite_username?: string | null, full_username?: string | null): void; +} + +/** + * Toggle [`crate::debug_log`] output. Hosts read their `truapi:debug` + * flag (web: localStorage) and call this once during boot. + */ +export function setDebugEnabled(enabled: boolean): void; diff --git a/host-libs/js/shared/dist/wasm/node/truapi_server.js b/host-libs/js/shared/dist/wasm/node/truapi_server.js new file mode 100644 index 00000000..b85520e0 --- /dev/null +++ b/host-libs/js/shared/dist/wasm/node/truapi_server.js @@ -0,0 +1,534 @@ +/* @ts-self-types="./truapi_server.d.ts" */ + +/** + * JS-callable handle to the TrUAPI core. Constructed once per shell boot. + */ +class WasmTrUApiCore { + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + WasmTrUApiCoreFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_wasmtruapicore_free(ptr, 0); + } + /** + * Drop the currently-paired session. + */ + clearActiveSession() { + wasm.wasmtruapicore_clearActiveSession(this.__wbg_ptr); + } + /** + * Tear down the bridge. Invokes the JS-side `dispose` callback so the + * host can drop its end of the wiring. + */ + dispose() { + const ret = wasm.wasmtruapicore_dispose(this.__wbg_ptr); + if (ret[1]) { + throw takeFromExternrefTable0(ret[0]); + } + } + /** + * Build the core from a JS callbacks object. The object must define + * every host capability the [`truapi_platform::Platform`] trait set + * requires (camelCase property names; see the source for the full + * list). + * @param {any} callbacks + */ + constructor(callbacks) { + const ret = wasm.wasmtruapicore_new(callbacks); + if (ret[2]) { + throw takeFromExternrefTable0(ret[1]); + } + this.__wbg_ptr = ret[0]; + WasmTrUApiCoreFinalization.register(this, this.__wbg_ptr, this); + return this; + } + /** + * Push a SCALE-encoded protocol frame into the dispatcher. Responses + * (and subscription items) flow back through the `emitFrame` + * callback. + * @param {Uint8Array} frame + * @returns {Promise} + */ + receiveFromProduct(frame) { + const ptr0 = passArray8ToWasm0(frame, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.wasmtruapicore_receiveFromProduct(this.__wbg_ptr, ptr0, len0); + return ret; + } + /** + * Push the currently-paired session into the core. Called by the + * host shell whenever the user pairs / unpairs. `pubkey` must be + * exactly 32 bytes (sr25519 root public key); usernames may be + * null / undefined when the identity record carries no value. + * @param {Uint8Array} pubkey + * @param {string | null} [lite_username] + * @param {string | null} [full_username] + */ + setActiveSession(pubkey, lite_username, full_username) { + const ptr0 = passArray8ToWasm0(pubkey, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + var ptr1 = isLikeNone(lite_username) ? 0 : passStringToWasm0(lite_username, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len1 = WASM_VECTOR_LEN; + var ptr2 = isLikeNone(full_username) ? 0 : passStringToWasm0(full_username, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len2 = WASM_VECTOR_LEN; + const ret = wasm.wasmtruapicore_setActiveSession(this.__wbg_ptr, ptr0, len0, ptr1, len1, ptr2, len2); + if (ret[1]) { + throw takeFromExternrefTable0(ret[0]); + } + } +} +if (Symbol.dispose) WasmTrUApiCore.prototype[Symbol.dispose] = WasmTrUApiCore.prototype.free; +exports.WasmTrUApiCore = WasmTrUApiCore; + +/** + * Toggle [`crate::debug_log`] output. Hosts read their `truapi:debug` + * flag (web: localStorage) and call this once during boot. + * @param {boolean} enabled + */ +function setDebugEnabled(enabled) { + wasm.setDebugEnabled(enabled); +} +exports.setDebugEnabled = setDebugEnabled; +function __wbg_get_imports() { + const import0 = { + __proto__: null, + __wbg___wbindgen_boolean_get_2304fb8c853028c8: function(arg0) { + const v = arg0; + const ret = typeof(v) === 'boolean' ? v : undefined; + return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0; + }, + __wbg___wbindgen_debug_string_edece8177ad01481: function(arg0, arg1) { + const ret = debugString(arg1); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg___wbindgen_is_function_5cd60d5cf78b4eef: function(arg0) { + const ret = typeof(arg0) === 'function'; + return ret; + }, + __wbg___wbindgen_is_null_2042690d351e14f0: function(arg0) { + const ret = arg0 === null; + return ret; + }, + __wbg___wbindgen_is_undefined_35bb9f4c7fd651d5: function(arg0) { + const ret = arg0 === undefined; + return ret; + }, + __wbg___wbindgen_string_get_d109740c0d18f4d7: function(arg0, arg1) { + const obj = arg1; + const ret = typeof(obj) === 'string' ? obj : undefined; + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg___wbindgen_throw_9c31b086c2b26051: function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }, + __wbg__wbg_cb_unref_3fa391f3fcdb55f8: function(arg0) { + arg0._wbg_cb_unref(); + }, + __wbg_call_13665d9f14390edc: function() { return handleError(function (arg0, arg1) { + const ret = arg0.call(arg1); + return ret; + }, arguments); }, + __wbg_call_dfde26266607c996: function() { return handleError(function (arg0, arg1, arg2) { + const ret = arg0.call(arg1, arg2); + return ret; + }, arguments); }, + __wbg_call_faa0a261f288f846: function() { return handleError(function (arg0, arg1, arg2, arg3) { + const ret = arg0.call(arg1, arg2, arg3); + return ret; + }, arguments); }, + __wbg_error_f085d7e62279b703: function(arg0) { + console.error(arg0); + }, + __wbg_get_dcf82ab8aad1a593: function() { return handleError(function (arg0, arg1) { + const ret = Reflect.get(arg0, arg1); + return ret; + }, arguments); }, + __wbg_instanceof_Error_b3f7e146d654031a: function(arg0) { + let result; + try { + result = arg0 instanceof Error; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }, + __wbg_instanceof_Promise_09012cfa9708520a: function(arg0) { + let result; + try { + result = arg0 instanceof Promise; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }, + __wbg_instanceof_Uint8Array_abd07d4bd221d50b: function(arg0) { + let result; + try { + result = arg0 instanceof Uint8Array; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }, + __wbg_length_56fcd3e2b7e0299d: function(arg0) { + const ret = arg0.length; + return ret; + }, + __wbg_message_324ac511aeaf710e: function(arg0) { + const ret = arg0.message; + return ret; + }, + __wbg_new_from_slice_269e35316ed2d061: function(arg0, arg1) { + const ret = new Uint8Array(getArrayU8FromWasm0(arg0, arg1)); + return ret; + }, + __wbg_new_no_args_f476b292f3fd1dc0: function(arg0, arg1) { + const ret = new Function(getStringFromWasm0(arg0, arg1)); + return ret; + }, + __wbg_new_typed_c072c4ce9a2a0cdf: function(arg0, arg1) { + try { + var state0 = {a: arg0, b: arg1}; + var cb0 = (arg0, arg1) => { + const a = state0.a; + state0.a = 0; + try { + return wasm_bindgen__convert__closures_____invoke__h9632106b2100de3d(a, state0.b, arg0, arg1); + } finally { + state0.a = a; + } + }; + const ret = new Promise(cb0); + return ret; + } finally { + state0.a = 0; + } + }, + __wbg_prototypesetcall_5f9bdc8d75e07276: function(arg0, arg1, arg2) { + Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2); + }, + __wbg_queueMicrotask_78d584b53af520f5: function(arg0) { + const ret = arg0.queueMicrotask; + return ret; + }, + __wbg_queueMicrotask_b39ea83c7f01971a: function(arg0) { + queueMicrotask(arg0); + }, + __wbg_resolve_d17db9352f5a220e: function(arg0) { + const ret = Promise.resolve(arg0); + return ret; + }, + __wbg_static_accessor_GLOBAL_THIS_02344c9b09eb08a9: function() { + const ret = typeof globalThis === 'undefined' ? null : globalThis; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_static_accessor_GLOBAL_ac6d4ac874d5cd54: function() { + const ret = typeof global === 'undefined' ? null : global; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_static_accessor_SELF_9b2406c23aeb2023: function() { + const ret = typeof self === 'undefined' ? null : self; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_static_accessor_WINDOW_b34d2126934e16ba: function() { + const ret = typeof window === 'undefined' ? null : window; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_then_837494e384b37459: function(arg0, arg1) { + const ret = arg0.then(arg1); + return ret; + }, + __wbg_then_bd927500e8905df2: function(arg0, arg1, arg2) { + const ret = arg0.then(arg1, arg2); + return ret; + }, + __wbindgen_cast_0000000000000001: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 312, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__ha3786d1373b8eeae); + return ret; + }, + __wbindgen_cast_0000000000000002: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 388, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h9f03fbb9b0a11e7f); + return ret; + }, + __wbindgen_cast_0000000000000003: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Uint8Array")], shim_idx: 312, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__ha3786d1373b8eeae_2); + return ret; + }, + __wbindgen_cast_0000000000000004: function(arg0, arg1) { + // Cast intrinsic for `Ref(String) -> Externref`. + const ret = getStringFromWasm0(arg0, arg1); + return ret; + }, + __wbindgen_init_externref_table: function() { + const table = wasm.__wbindgen_externrefs; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + }, + }; + return { + __proto__: null, + "./truapi_server_bg.js": import0, + }; +} + +function wasm_bindgen__convert__closures_____invoke__ha3786d1373b8eeae(arg0, arg1, arg2) { + wasm.wasm_bindgen__convert__closures_____invoke__ha3786d1373b8eeae(arg0, arg1, arg2); +} + +function wasm_bindgen__convert__closures_____invoke__ha3786d1373b8eeae_2(arg0, arg1, arg2) { + wasm.wasm_bindgen__convert__closures_____invoke__ha3786d1373b8eeae_2(arg0, arg1, arg2); +} + +function wasm_bindgen__convert__closures_____invoke__h9f03fbb9b0a11e7f(arg0, arg1, arg2) { + const ret = wasm.wasm_bindgen__convert__closures_____invoke__h9f03fbb9b0a11e7f(arg0, arg1, arg2); + if (ret[1]) { + throw takeFromExternrefTable0(ret[0]); + } +} + +function wasm_bindgen__convert__closures_____invoke__h9632106b2100de3d(arg0, arg1, arg2, arg3) { + wasm.wasm_bindgen__convert__closures_____invoke__h9632106b2100de3d(arg0, arg1, arg2, arg3); +} + +const WasmTrUApiCoreFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_wasmtruapicore_free(ptr, 1)); + +function addToExternrefTable0(obj) { + const idx = wasm.__externref_table_alloc(); + wasm.__wbindgen_externrefs.set(idx, obj); + return idx; +} + +const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(state => wasm.__wbindgen_destroy_closure(state.a, state.b)); + +function debugString(val) { + // primitive types + const type = typeof val; + if (type == 'number' || type == 'boolean' || val == null) { + return `${val}`; + } + if (type == 'string') { + return `"${val}"`; + } + if (type == 'symbol') { + const description = val.description; + if (description == null) { + return 'Symbol'; + } else { + return `Symbol(${description})`; + } + } + if (type == 'function') { + const name = val.name; + if (typeof name == 'string' && name.length > 0) { + return `Function(${name})`; + } else { + return 'Function'; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = '['; + if (length > 0) { + debug += debugString(val[0]); + } + for(let i = 1; i < length; i++) { + debug += ', ' + debugString(val[i]); + } + debug += ']'; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches && builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == 'Object') { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return 'Object(' + JSON.stringify(val) + ')'; + } catch (_) { + return 'Object'; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} + +function getArrayU8FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); +} + +let cachedDataViewMemory0 = null; +function getDataViewMemory0() { + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} + +function getStringFromWasm0(ptr, len) { + return decodeText(ptr >>> 0, len); +} + +let cachedUint8ArrayMemory0 = null; +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + const idx = addToExternrefTable0(e); + wasm.__wbindgen_exn_store(idx); + } +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +function makeMutClosure(arg0, arg1, f) { + const state = { a: arg0, b: arg1, cnt: 1 }; + const real = (...args) => { + + // First up with a closure we increment the internal reference + // count. This ensures that the Rust closure environment won't + // be deallocated while we're invoking it. + state.cnt++; + const a = state.a; + state.a = 0; + try { + return f(a, state.b, ...args); + } finally { + state.a = a; + real._wbg_cb_unref(); + } + }; + real._wbg_cb_unref = () => { + if (--state.cnt === 0) { + wasm.__wbindgen_destroy_closure(state.a, state.b); + state.a = 0; + CLOSURE_DTORS.unregister(state); + } + }; + CLOSURE_DTORS.register(real, state, state); + return real; +} + +function passArray8ToWasm0(arg, malloc) { + const ptr = malloc(arg.length * 1, 1) >>> 0; + getUint8ArrayMemory0().set(arg, ptr / 1); + WASM_VECTOR_LEN = arg.length; + return ptr; +} + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = cachedTextEncoder.encodeInto(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function takeFromExternrefTable0(idx) { + const value = wasm.__wbindgen_externrefs.get(idx); + wasm.__externref_table_dealloc(idx); + return value; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +cachedTextDecoder.decode(); +function decodeText(ptr, len) { + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +const cachedTextEncoder = new TextEncoder(); + +if (!('encodeInto' in cachedTextEncoder)) { + cachedTextEncoder.encodeInto = function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; + }; +} + +let WASM_VECTOR_LEN = 0; + +const wasmPath = `${__dirname}/truapi_server_bg.wasm`; +const wasmBytes = require('fs').readFileSync(wasmPath); +const wasmModule = new WebAssembly.Module(wasmBytes); +let wasmInstance = new WebAssembly.Instance(wasmModule, __wbg_get_imports()); +let wasm = wasmInstance.exports; +wasm.__wbindgen_start(); diff --git a/host-libs/js/shared/dist/wasm/node/truapi_server_bg.wasm b/host-libs/js/shared/dist/wasm/node/truapi_server_bg.wasm new file mode 100644 index 0000000000000000000000000000000000000000..7fb39588d1b87d83aa418bb32d2e30fffc79d3b3 GIT binary patch literal 943201 zcmeFa3!Gh5efPcZ=ggdQW=@hx7)X$P4nZbLWHL!+6408>!&N|els>i3$M+qQ%mkQ; zgbV}4=S30}6>C~iQBko*izQW5+G3@a)^MrPii#C0)>Nrdr4-#~vNUfsLD!}58= z$|KX?K_amPDVIx5$>osck@C0k$FtV_ZRZ77>Kl)yUCSi$26$5oSE{pZ+oxb7)Y#H$e5BkmKja#>luUb7ix^`my zmQ`z3uOFVA*z65F`8N|=CMHKlM@C0ShsQRrUq5;%{cPE|ad_j%=&CIv>(*^qw`SC9 z&fm`pH=heHx@30aMVlwLZ5cmjX4@+-ykv5Gc>VCIk0oQM29KMcc+V&YXYA_?9(eqnk$7k8D}JWn$ID=HUbNcclAt>*Ph}&t5RT zc67_;iPe)Mo5m)GN7k>OIMDQtbzfaPvu$>p=6A{DtS#i)E$cT;Y+g4pwsw;Fj*WVU z&p+T}+5_GKWXCVrdg12D@sZK>ldIRRUNt&7F)}ecy6QkLck8>mL`2N=~yl8e^HZ!yFRpT2s zO$?7stl2a%GO}u7?eL}pf$dqYkylP$a>>T?C&yQhtl7AE?W$E9CpT^xTQxlCEtwx$ zBknVharVN^;~O_`=6kT?w6k7vQg+t(xu>7;vhm@O)oa#lUcYI0{p64voG8X=5M@YnsKh5 z+_-M_=CLiqtJaT!J_n%C`M(}pH?bDPUAubY7MRJFwFg3g9C{iI!%zIw~X)$3Qmz&B59TDx}3x&z_XQJs^x=)&3Y$;)RaXRL|Y zjhnVkdbRnN!(>#BPn$PhGCMxp{oeuk?~{Evav;Y2WFM~nWj|cw^@YD%+Ewa}qQv)0 zy=6Zr`}!~7f4#l_LQ8nD9|cJeAMN+@p9(4V%T$i^KfPI6AC;pp^t~{QR3)G^^!>2M zeX`v5qaHu@efqNh!pI8}&t8v-BEI7v|CPNUw3mfm;4e~VN(CY91wkB^gMb#4ML%IV z^y4^YND*a}hn`0{|Mh#+?;+*Ua=g^@p6!QTaO|SM@Adp%TBAwtFjrTvQDh0mtPdGl zsP6d0_iLda2430oLa*cj4RRyt;bcT!B`DEU|~?bwGAN=X|h$fl{ddc*J@#zbE-G_UMwS3)o(NKmYTGp3{Gv=Pg@#qzvb)$1v--nAOlqNudi^F}qvXl$KTS+IVgAA8ABJ?XPZF9_oo zM5Id*-v~?WKfa6N=+_R1LLiYyiwGvt*kPVu;y)VpmZ=iGkR6ru$_oS48>=QD84fV9 z3ZRQB!NvX$!yks*yq#qx<1J1W1Uvj}=gz%eWrvsT+IxprDP4E?i<4JQzH-~ls{(Im z%a#kryUmN4OW{jfE}gw}W^#P9bn&g@TUL*FKMxPF?&z9Bu6yCC(L=BM6vl9&jp5is zn%jlz7{YOfRQ2L*=%14_)E_>i`Uc5Omu}f&-y9nFE`16hx%4T3N#$*?ym;&6?4%*m4-dUIWB$=0*A_B;|Ilj#yB{BVZAS9glY*p}B`EcS z@Og&_k^-rI9F82~7oBMJuZKJZA=Y;fz4lWAkC5xo!2^0d5gKc-b1S?q&mc!KqYUc3z}Od{pDi+A{gXl?+#cFpV%b`t9$a07^4;c(XmO}#r$nt~eNsz@RX2|k` zs5w6!IYIgDx@7u}pgSLlnMXp}&Uz2&H z@TC`BsE;>qyJ&W1-LT z+?W1(cFq~&FFWO>|KpUGhW_Es@ck_|1f1fxc^jIZP2q{KAGhMkGN(_uNOtUm_p_ui zr{-qd2QwL_|7DXe!z-q)|1?}Or_#iQmt4HXT67#qvm|d*t+}uQ1Sv*882Kk0t(=m9UtGkb=xHMeK~HVtoFwQ6!~i}xo_wSm#qBddn-@~#>lo|s%c;eGZgHn4H^*t*e)RjbEV zZ(27wxpC6_r{slCX$o^27$2dD1s(2)S2~pa<78n+=FFta{bVxGkvn<$MR-kEeeVa! zVdHas7&-1g7%cAkB=>V%;``>&%bgh$lek~Dy{dq_w>tsN<~T*hC-PBzJg#>%iH~(= z);p?aZ^`#Ezw&7Kqu}BAk>rN>vG}j!pTzG>ej0xw`OENkqtAxF6MZteD!wavUHrxH zebL9Gx5W2^S4Uq6uZ!*qcSV05T^s*dbWMDBbalKp`knaB@D0&lMsJF5i(VUlCVX%7 z58*qaec{`quZ8c4?ucF!e=2-i^y%=1=-%*k(f#3XM}Hl@Ci+D5hWJy_8{@Br?~eW= z+7;g(?Tr64`rY`x@cq#P;qOJahp&(RK73-@L-zX-k>ta*Dl^p5b2{+q&I`)IiGfnapi4}udv z77nj|GRx@ad4ym@$moplkk6iIk?JyYxoKO3(0rmH-uN0o=Dyke>l81c~A04^lPJh^n7#jnE$=_hyKU>&n0Q| zd(poJA1HmGbVvN>!42W(;@=MbH2FyI_GCBzzdra<{D6N$^8Vm~I@HGUwzo99=^`|D(H@=$zFd}nf3 ze0%bD@#m9!lD|*Bmh4OZA^H92*7&cIuf>0nd@T7!{O071$vr{i=Cxodfh)hZ^ya+g7n1KKk0v*jzLfkh`B>@h`0K&<7~dY? z_iq0GgZPc%TT4Gm-jw_}d0Xkz$@}AL!_P)HmEM)SrS$&fmiWub{mDB^Z>Oz)i+?w~ zuJrxrvE*U@r_p;#H@V4Iw3ls|YL=FJ!G_{r>Sv8Jq7A8+`e~4c^*04ozmf)7b-J-E+>l16w97I-P3jj_ zgNiC=;bqMw+5O(usYV~^CD|}SE7duh6w z_GW(l=LwAt1;dtAZfAA()0g+NhW4!4njY(#y?Uhd#h2DWa}j-&r_)7Co4uKLdc#IB z*jz+^wRBNue>EFIEzbfDX-~RjDUk7A1Wdfsr<*;DhD3S#5|TCV2se2*Wlg?4sMcI< zPz5Mjvu@S|gz2`$j)D`~qxwW!t zgUt#}^`wr%IagHeM>X%#WfJbPs1~7e&*4%2Yv-$Qgv(d@TulNV?ny4Q2{eO zz6__E<;+tfXD_9~RCADCqYdE(zAS^TeM3RH*{@G)Sq!L|a&w^|(CpD-^khCu%isFz zOU?QzHE=Gij|R(l^o$0Blmf#>PXP}1Oc!QP@Ly?~m!mG-4!-xv^A~!Rw4QkEyv{H>LS z&B43Y1@A+Lb*&55^!v-ey8lUGy|5eBwH($9;RD^Uu64k=HpOCez`8c2#caX4E?C!F zur51TmmRFj6`C%0!W!rs{FcF>SYfT5qb%cIgUz~;Q9Y-u!$>VofwXv{776xK-3kRQ zpjVov1!EychzuB4+9&FV5A=YDXQ)^mhDg2-Uq4@+kr9k50awP1}P>VoyJNDy^sqS7RsI=;Uk zS}IaHqJxx{GYKbN=ie|kdo-QTPi(6N>7tc_vECM7G#9x z?)19h>E?nCLK44onq9UWCBIX>tF~kkOao%X^pNNpo@(}`gJ+q@VV0EHL2XMa_wUG(s$s>?BfvVXD5%XWvFWtDRQ`v9+EEzTra&5@p(C~k z%}*<*(VxsRk*^UQPLrHmD^elb)|ly+I70kJ1v3!|Pjz$icvyB-r%d@ccp9}}1J0A) zQ)?cSjAdV#fc{kKtGilu<#t>sho{S?aIsYyeRv-F(n4`nhaHNm^_mVag@4E65nf_r ztmh-_%LZ^Mv>8fIHYl%MAFd4s3W%~`s#!kGm=XIvSYkI)Z6;xQ=pn#)1PV-cq|4%;P$GaxHQ}x5i#U0h7kQfR*Xi zB86iVn8se2qeWJxn#{4~&WMG`H>~3v)&t;NH>~3h zSc7UE8ap++T}O^vv|zoT>j*!Y3K{$kO$seU3iY$M=A_W#E;_!zd_r4H*;9;0%n30iL1V`3On7*x5t=h!@sV?A}ai~j#^Jsp75I;pI~(G$ySBPugrQBGxkxv?M)Dexq1U z74sX#ovs^cu+DX3!%)abr{AbxE9N)qWGnI;F(c8A`@j&C*gzGlqa6_;mja}rPCq#PoXfUAIogjPIn@XRpZhSqWp`g0J z_ljZd|4f6S;DY9n*;PAt?!5feXag}pu^RFiQlqIp%gsT(KaU=d%nYggR+3amQcShS!nW_0WR|tNb zYDPj`MyH4$fvS{7a+Z~{C8NQ~q{{X0;C;#NW+CX4eprV38n^mS>FyxA}U_Zeyb1M{HGg9(nax_#oLQ z`D4K>WxGJhQLVFlwOa{A+Ib~Wax;y??8^uJDZ_>A9#~6n{aS1I9{k$s;9kGcujhS! z<5|R_z9-He`=`BLVGvp$vWet569 z!t=)HOtE^|^_}T!TZzEdtMhNl(s*eoj0wRBQE)XI%-_m1Hb^KdtEZq@_4@N9`+-Ko z;M{1mwvlTfnC=TW6y2^$M{twuU%Q5H%V{0Pr_yeAY~xwkv2H#@8ZJojgid95u{`;F z`sFphy)(^xZ|zJo-*#astC??Nj8%G#B)7uabEIT-tb2yjxgY$e)E;%S`;s=KE9xy#cUWi)Y z7+zB$t%-wjq`c`gTH1^RW&Y$)X1m6q417c9L>1n=jY<2~<0xPve@dl%`Py5j6Dt%U0lDrof3j`r|GE6_$K!ZUcsCywQ{Xx{26|sD7XR=Ap3HtZ7G5Ly~w39GJgU5IbAv$?DcuF@p8MK zWyu3nf(6-~>Su8dI;fbS;aYW3p>TxtS2ChcBQrFz*ZM8yEn$PuMP~R@66Q$gAbWtk zl;?GRp#?DdLRDLmjN4u(ox&^(4szCPVLp>ZIw z>W{~1vExzQnne~NYgFYHhDEL}r0z;@r{5S>8y&zPb-mwMMoV8uaP_Us#ZPu0sVLVb z7(1yH>QG?4M$L#%^!K?@OW@qYu5V*e8aD>g5Jq-PV}atUYw9)vZBF|B-~7Yd?*87_ z{^+~jOKa$)`6_-z(Cw)|Xmg!)%XhI`zO5S4L07StRV)LhWvpVoSdk=qP;K_N+B`(()KN~Cnb4{*izrR$VX5_y{Xi}E7PwHW=hJXV@W~tr-Y^FVB!aJ< zg9Ho6yS^I0K=Rk(X8WT$lURl`&zJP%*V&DT5^*|l@1in~IfbINWa{MSfjQ^Wz zC~0BJ(>4BDCF?oB_(6pzdsy8r7P0|r;Q(&ArwIQ9qYUq0-I)N{=O5KVI9gUbK<`Qq z`&1$^JzS9&Wge0qWlj)CsP&@b!kBoJVN-P{S;P$Mw$D7c73F_OiP2y=4-VTLM=g}P z8vWjanOwyftY(ih^gaskMlL4#F_;F2AZWDyao=(HU_)AkKNIJ&`OsHy-Nav`oY1d1Mr`rkRIfoqz*`$}d zo@IcpNTnBPvQ@kU&FAE%rIPEWn$IQ3#@V1%#oHa1_O_BkQ_YoWxk%DOc+OauW_x|e z4P|4Al+z{YLbbd^rRf-lU2wZjr7QK3vK5PSTr06`Dt&G%v3x3hPAhTTRJx)yNX)Y; z4iW59O%f|L;=zfu#V9h;@DT3|K3!}usuLzuMdL|vdDtpA`1f_fe<$kW;%p5%z28`f zrdh1WV(igYuWmsWqbRpvFHG&B+rf60TaLwa*s3MHplpLHvhtywgI=Gng`c)pp}hcY z*tfwx55zU2C|xLX7jR5z4=RhYVI8NUq}*7dQ*BEa1iDz-C*T0L6V6QQWB3f~kfmXM4taSSI5MQ}@$VI2 zCcbS&0DT%!a#Tu7Tr+chuMmbNpf>D$XqGAN^_}Oc&LXvy!Nb!c)a!7~r=G6`lRaF$ zDfCMVLAqAIH%j?4fwXnS~@B^)jct^*K<8ah5V%#g(0XPt8M zG_d??CF?x^%S0ka+5Z;H91zPaXB8?M+kC1_Gp>e}EJ2OKZjG|He2*q&ed0m*rJBPo zrY*o0WjCuW<%^D*C8sN+MG=SuuyFUSq|4? zgdfFLM0^(#|LGEcQ6=j;z*=%>v7e%7$n&xvKl~T3Eunx(Il_~6ue5KN8>%-Nx!g|9 zZ#9hD3RTIxR`4sV9_O!O62QhBSgP*j?kfCNZhpDT>cQM)^^osWzlVLy!?rP@^)2YO^h5V+hl)K>y%zIahfe?`JWHbp zR-X7xui%l42AA_dyTzv{?N0j9x$v8zBszt)*rm)hQtJ9I!HCLUq<8{<;RebepPuf`|cD%N^>KwFSWT1jf zSN*S>vhXrRg7t2n#z%r0WbXa+2+5iubDJ?Vc>{5itUAemb-K!0Fy6j8U44@O`mWZ` zkxGCdX39-w-OJEHIAI+4YK~~Sz)4o$3)@%`oaJ2c*at(w>0nyof=#2g)O;TEH@Eyv zw1GITzsGls8kpp+P^r@(O8&8kYtU-mNf*dXD!Sf}sQ;a*_-6^F=}#`8g?c*&tK|sO z%~=N*vK*vGj0O*D)gjb}^n_3!)LJr+2Hv_x4|-wU?GY$A3Vjbz6l73ivsQn7&{&f$ zl+Q6q*RX!Q>6-c0PhP*q&XW6=k3_ttAb! z0llIh#7}iqiI&wod(>>B*H$&xZY0C|qy|PjfERL?KAN@xP^L>;AlMG`E6W12y{v_( z74@n5f5+3QPX~Cck(;G2t(owqNkLTl1C7zBwgBqd0oY+{#;i=W%I(8fh+W}2Z;S}9 z&J|usxp+kyw3Lf;S;bXjW{fh()6*UeGSLIqa5{2RGv3~e1o1iKg3}0_0gBOrfGWKV zi511hOw03sVbqZn3xUFB^^Su;dx=v80*R^Mo&6Xg(mkB5(F2O;ZLNUXLA0BcJ<(l&yDIailL6Aq9@ zg@H!IP76YIPMg~zcn55oLZ+O0L4-l()8H&676c0vDla~eZoub{WxEQ*^W>Bvbhd_!{;Jowe^x=eHAB=2QLf>@Jb zw&+Z3VQXL|6Hw!(sfXY=|CO_ zn*uQcBKkn?h9W~NdVzRtjP5~`fEbK4re$(Yj1px>&N~Pb;haU9fws~#ysGn}fgMy7 zlRZm#ML{H%pYM7L0{FU^O#Rmd@|YI%#1iK(5X}c;BP9oPEpm*HkANNL3;XigK#>>Du!D`WK#F^qeUjpa zC^&l#*apa%#}R<#9`ca~+F=zt$nNV*v&Zs4XPUjV2Rqa3w>{jM9-7J?C7tidm|~$R zKF9cz2x=$mDS%7KL>EQLR3S>H)c*s5K~(Y}tjN&|stT(qf)OQ1s~_%*Yh0h=BUGrl zMpLV%N9DDu93K>%v~|y(d{Lod74i-Fs-27Jv6Y8xIRN9^x`M=hqk$4hi{TM=sK{Cc zd=09E4KTPTZ8%OJR$v=k{Nl7_#4m)+h#Qikg~mm$O?F;L91H|DGr>Ug!yMW`C{)Ex zXmA}iw{o|k5w@X0?y-#OC@9y(stAaH5mKIppR~gr;ylm+{Lb|pQM;s-gT4_&{d$;9 zPGW3!PdATsB$ls2&;?|WxmXAnL(xv*+{t|(@@^ifErH_+X)&z|_jZsTCTC@kE|&*0 zNRO5WGf1DM*bXl8(a|qdave!)GC;=+xa8;D^hl;!G*nToDq5ktoi1N?@D4R9t>)Ke$iGgo3VkIktFN zS-iAz7CKrm8F!8=eOzwrF`7hBW{)Uw>ks#O^$(fEItd>Z6k&dBdB?CYv-`hrXPYVJ zV0oXStPmHj$$Nf!hgbi1H<;(REaV0lsfDR3vh1$4LEny0@n3tvO2vN}n$P*$VyJWS z=d0V!(^vMTBFP>B4G1MGFY-+Q<6Z6r!LZgr2-7X_vEdgNQc43wA_b}Ev<+dhXPJXq z^oRYXE{sCmWSdE5vcfbGQ8}5X)>e18eUTj+k6IIhU2;<$wj-KJG&f!2-54Pd+QSObgXQLh*o(((16Rin(e5Xe>@jzBJ8 zI1FTh2g5&)2QuRo6kipb==~;1AUHS{uCupb`B-qW-ZH|vnpC`)*A3JBDZHD!-ir>Q ziR4y*Ck>7Z#?+k{S)LXeX!MLqi`uimQ`r!AP`YuW8Ag)HWL>NoXuFod4+Rsh{DKO; z9vkdVpC=OT3LfT7u?fx6(bWK1>*;u@%oX5V{|cyp>lM&6%PT$Yo1y)m;FUoU6cc!p8D3XjomhlRQl8^O)pp4$w@P*39x=kS=^( zvo3c^{ebS2y4)!m-!9*bMDE9NPL+@yiS13Sq*+Wb**(3ofT(y414daQ#w_=%)a6&H zzqUzJCLtE23+DB!XzviCgp-&R#?R8oy$%Sc=>riiYe^KKM;EFSxh9OksYT$(-M+@l z;cx-ak;N%E*Ya^WsY1gAz|oWha5gRX1NbqpAA%UANb@HLI8gsk00lg(L((Oc% zJ{g!t^v&z@wPYqhY-B89d)$&CDpovNATz>vFWxE}z!1~GqLpE$Np@Gh!Bx&Yddg?W zJa+m`$hF+GgK4qNj89!GD;r~F&{EAIwWt=!0b}uAuuHpTPUsEq%r$ZROr{(^p9ey9 zUFxn6jM7S>#*oV#4Jc^vek)GafJJ|bYZVOYA`-p8wnm{M642+h5S7(L?rJHE+S~=1 zy~cnE1gkPBaFy7jdX&C!dNq{HOpLX{!-A8?LbS-CU;}d#)R_tJfKbJAT@;J_kU)|~ z2{1E3PexQ^n8EI}N$dy{cjj*~mSLUhYiO8>q#So<;=(imr?(Z(csVS{Ap$c7BHM)n zxqY~9s{Rm);R^hukSb%`0~e9)Z-_JF9_mtV7l$$Rn}kO#jR?|Aueqs#pIvd`Zn;Hn zP##>KNfm3FfcLDa-DbzqY9v}y>`L|>OykzDnajE~p+n*z)5NW*XaS6IVQtnT5rifJ zgn}kg?86NVi#`=f#J;NhL!eYzs{d}zA&r9_grHE{9B(G*C|^cI;MIamn>_OF>f-Q4 zca8?>u>KIlA-6UwA&ZeY%ugxZWTl7@CV91Q8Xs@4D7d^4C@v*%EfdHc{?!gYqPnb- z?fN!)ii_{o7PVlX-HqI+6>{QkCnriZ34d$diG!We4@qCqsOoV!t~4IE03&Sx@w!)n zq+H1!D|*QGed$gwTbo^XKf&?8BcTaJI?d+(q7E`X*MD|mWEBHr=-2xY7C3n>syT+R z$8DFumgffX%la1su|s;5EellOEgq{U|FoA8J@=br8!kHJM*f|1f`>^g<>VD(!8)~x zLZ?rzH|jdKp_nbUtDW&KI&+BCWS7b)C3PC;d#Kt4N!BI^b}{gg@juf>3~;>hlukG1+UP=HI)Bp z^PdMg%0I8Z&#x#945^y4DF_|s4?FOq2tA&VItYV0VivprZq(fa56o?PFWW5@jV@Xl z(lJ=bmEP?pkU(%b&k%)I=*HI|1;H->uFP(?Ptptkw=tuQv0KSgnQgD!`yCk$dt5bS zfEW_9HrE1#>ee;Z_L!I5&Nx`nHi3!xz@cvSaY48xgK)rh)4Cai011pvcaM|x5QDQ! zqd?uAYN;U}fn2fnV0~74*ND~HsskW)l1}h0$BT`!BF2EOkGulW%<9*GrMeM3rl~;8 zwz<`dqyDe`0FEiiCz9snMj!45DxIzf;B}ylrPfg^rUH!b1P>-g=)Dy&-eC@1s01Z5 zm7~Tn`jy_v?#q)!0~9{LsQ>&lb~&^t86*SHnmrj-Y5m74@HqVKL@-Y$uWytHMhgrz z2qrB@qbkyR8G@H`0>K9YV&UM)fJp9i(X7L%>??QLCe`uDwH+;1%U08B6{jDvpfwk5 zo&57X>|9byh}AW5(oEcQEcJHZ-L23gg1rq+DokKa5qBkL__5#P+9lu_Pmop@P~~T> z00$&(U;&aK`$>CcLpv~$BTbOO0lL6N8ub_GT$h8&B#oTe z2L%=GvHz1n1-%JOzvu*$4rV+Fm<;E!Br5ecfW|8!44qk02+B1^5x2SRcA4wB-g!$q zN4E1GwhJx?y_|cxASBR#4k7LFyD14$>!PHa&8KIi1XgyEQeM;fEL}CDC977@68&RP z@>h{vVplE2_<}KcS>=?Lmz8`WOR;gJ&HsGoNHkDv9T{ENT%4@2pgzq}3__8pF!Buc z*V)ev0OA5zJxWxE9U$%kKdkE191EmVQz>FbG3<_7F?JgHrc4otpH|>!eet42{7|Lz zDKRHGAtjj2CW0~+=#{o|KoJ>F8d;&xaw`EDw0jWb|I~T6Uo*`8nr1`dJ8AK6$qCd! zhKG_Vzb5CAv=^#_fX%X5t*v3yTf-n}s{kvWLui&kLx)s>RBHNyQZm0J zna_5J!A8>nsvJj@%*VVOwjz`Hpadn7`7L1at;}%y_T4^&Km)>X<1m!h#%Mt^$lh&}X6l`jzK%&>PcdKK z>NYiayRATJy3jx8h3bnJE?mfOp&pz7j80$^G=t-SSyHxGe0Xi0|n)zcnR!92wCJfi)Ef15MT%aj9)yv z?_c(M*>hbF*(NL4JFjTQ8+br$>u~uz>f=+RBa{qT_W=9c zdCZCWoqj4a&o&Z$op$5=;X$isoZ!fg$u%O*Py5+14v09+Z36o{Jn+`e-#e4WJzL*l zd?EBZYMVjwf}TJX~{Ota-La83&&aQX5mQX zH^m01{tBm~qZt3s{7pb~K_0gCOjL@zQ4S^p1x(1}=Oa#Movgg>?r7SM%Um`AJ4tWF zCuYXL{xH>O;`)1Gat@yP(+p1L<8fK{%;Tq-uk>8@#IJ*dD2QgZ_Zy<1Fzv{umemG;hdz^~-XQF4ywZ~SzT*%Ch zy#;m_>UudX#Y=(QlhtU!@Xl(KE^!v^aejcABUIFq*@Vj1W}tzKiQezevwArW!bP-i zMy-7ZuC=dS>!G=|01p1c0;eCM=5-cH8LG_|6g0s7mRoam%WWoJ*K)VVpFNCFqUwD6 zdL4@{|NJ9jkI1m)C_Av%eV@md^Jy=;k&P^K<`5=ie70b-CwFjJcu&6Lg=Qc{i@$hU z#A0uIc2L>jYZFVSGh$i~8YR)yq5fm}5rjlh5C>E`j@Yao7n{{1#)MB88960dL}dEe z3zgH89km^~-jj`=4d#}!WHwD^tY%Qu45$fJBm}yu{>8e(GNEeKGD;Y?YOu10@Wkl`5M%X_`;HU9|6uflO*_W?hy&3&oa z@u%`p1&`hic=Rf6t$+vhfJU!Dqc)(EVg1JQ^#||pbe3op0jT^B@CXi5sH6cm zvrHVV>>)5pa&ZNtgJt);=dQwb($KTdw$oVCbqt4y3l|nb=fkLo^~aEp+U$q9a*|mJ zY%kI_JN`71Kyw-m3Ec>nDcw{6KjIlbD)_r2t)>2q%Q5EabgyV5=ay^2zcpSJz zbJ9xrQj4kJs3St`eUPn3BAuy&A!rs_wPpfIf_18jYN88oK^9J{Q<7G7r369P?kg&$ zNu#ROBZ^SpR$ceVvBDQIP#(X9lxjas*E!2TtTOnHxLJs+{E*L5+)LtWas#U_a zJzk>0DrmiU)2|MM1PR%OK(FkhzU*ibRA2T&arS~>e6$xW8Ll`+<>^8LZ3+t%1M&x(kgbr42R&BVR^!(jrl6(lQ#;P88B|pb zpP*|@<+NhND3m-KC~67!L&%crPH4ykOKw}jmx@K@sV2@KNqqvz^EadrTd%2aJGyAR zGCS6cVF<|m%{-sq-s{zMA5q3%)>N>hE|@AC%D!?N+xk8o3sRRiG)AC_)gx4XnNCy3 z<_GYz)~0eS>lg{tIye=}2q&X;Y$~?UHO;|x`H_}xxZ!y;9h-WGU!5g-n~YpYf?;{a z)pRP0n}Kn?ttVRiKl zcX{=H(XN9!6(TSI&_t(I)O(KHC1v>8b=1yo&?nzUe#$C4j|V%XK8@M5*fS!Mjfp)M zW0l+tlNFh(KLhnYw%DrknzBB@S*|gfz*?keYXk4Fn$sHMrDJ(cdIRdtqbv}ManjZK zJ4?j2g})3Sf(un1gu-g05l1JBD;yEbfu*Gj6v0x?9)g=$;0SD^C&yYKA*ixxHDLpA z_>~4HcoA`7|1tD6+!dm_)fRvNXi7kcpwkHnfIjaCp*azxl`dO zl84aVeRbRzp;z`jffmcF3`lM=q0M80W`Q;Xki41jwh5_NL$iSl58=_`8E6UO84{b2 zwBs2s5-%lcfd-QfJmgv(s|1Qk*>UXB&~Ug(BgRfR`)pSV(CpdLCI}H#n;T9i=8;dx zo_4%kB~7%Dc^JBBK8~Slamw|-!q~>db2x37RNyqNhIlNjo_VVSltB z4x`^E%sEV|HCcX0bcHyZOIHL$pEKsI1=o9x2Pv>nB7z45Add7ja3LCcfnrI3i}nd! zTt8q$u%2R?oKs%|kbvBl`r2NRQcJ5V&C%*$VvtQ%a$15`Msl-LqYLI}bdD=&Q++2) zSqu8qs-v%_#Lnpin#%pJ-Z0G$SG)et`XP9=aQY#|7GU zFw&L2&-bT+F8HQ=^D~I=XP*Z(;_US;{|v(9c>ij=V=mZw-g1tsQSQRX2}sE8UZBH0 z0#Xzmwp{Pa5k%Ct?c(CJwq0EI5N#LN%{rE+zy5z~+8c#sD?N!MoSPu23-7X7usNyl zE?PU@-_)qujEwZR0@@z8SiRZp5T4yM!35Q@1t{>OW8CiSgxX#n*XP_!^Oo=XtmxeWiAvP$3 z^D^C*!dyTeWq6wfk{fY)>baYvm~NqAxd73H&f)F}^i!6m#Gi+Uhr&FR6&EhJCdhe&^cG;(JuWeU*GfziyQ ziq;-P@=ntdIAJ7Wb4s~sshvu>Xbj3~Ddn=D9it;mOcKO^Cs~Bc5ctdC%xxVweI)i` z#lEy_sm>Q|$|t)@8x%rrKtu~PR1;=PjgyfCX>-A%^$*2i(@E+D$9W}G?-$iL!Fm(R z{JiXbN#);>hZbN^&CCwJ;W^X9iWf_LORzF z>&^O)Odi2)e)H1x$`@T29vO`)@(IBa!E9cfh%yaX-} z#Z)IcxElNLmk=QC=18T&qhF2#oh`!(-EP3|upAT-6kZ6nhZ6Xx{YmyhSZnziUc6ALkd>oJJq*xgMig+1;xE)lDVIe$CE-X4n4WPUeI%yWdg`{2OCG|F~GgLPtV|8YyGZmvuBqz78wuMoFFi#MX}D-Up-w^-)p=-)lLPJ*jVI* z9B%D?YSp=g&jehyxKYpEf5V+#0|PdP2+pu)!!wDitMb4TudAETQ(0^QM=)a5>!8#< zw%k1bZC)U`RY0@;RW9DK^DW+}TfDc9_Y1TEfD2kycD$l~da25n<61dku& zF5Dgv83(!ec#oly4x}tExP#s4-lJ`&#Vlz1Qf{yM785AkT--^F)Vfg zIWp%A6Jxcb&H1`vA-3tp0go8Xg6J3}d-B*{T5ik&8I%K#;tWGcdM%PIaEza3 z)p66Cb3m_+YlzquY1= z@J_GN;5Ht|f4RF9^Hnh6zHoh``vDHO`vDvos}=Gb%#L?3yXF8eJ51L40Jrs;63!ZN z)zS$bz}EMoaBkudg>woH3KxnZc_Lhq#U(;@+s+3~?m^1*aXDbi{ne8$Gv}jW)H$e` z!au~O@T1MC_F|;3py3NnF2oje{w?b_$NRQT_7Pywk5ytfZaqGIX<6#}AlFwALayzbSTNk$FConX0i!;~! z8BRw)k-%>U5ey{Mp9Q-(*;@ zvkicMX@A)|Cd5m!ul;;pvBA{lXV2#7RS*!J4WPSVBGh-9dtiSCgAfyQL?18)(-K+F zcU`~l0A1T10(O$!eW!yRy}Beus7eG8Ntmq#P+^@JG<7E!cp<&585tg!e@sDZImETR z7^`+~*Raztj^lyCKg0vIYB>)(Z<^>*on@pru{Ov989cxP8H~7<3*ZXjHr!zPaPufARB%Oe_G{3 zN;}1l`Bg+534dDUD1Hi_Xr-Y%yA9qg8^K3PyS))dUzL+S113m0{nq>=W^`E4Kq1Wd zDFlnGS*OyAfdA zo^b)+ggX5y12@dBp}kN<)eP$2jyP5xDlCA%KWjxnDy*Nz+&%27j9clSEQaj509*{S z>w>BJ>!4|(%&rLvSH`JnaEfhIle-HuK3`?*1Z$pXryTCq{CSS-2wuRuqMDHMxj-#%`UVV z?gzk6DF_PY>~}QnL?W89irxEshR z))}7@Lxf?qi^^l+wHhD4FxA6M_qfX@xa_~a6Cbs7ru#9!)X23o@__Ar1=}d<( zxUj@m)FD`UmN_l7!@Lbr1GX)kjbBO=cyxPkS^N!md0kY%`CR?B$AGFqI5<+Y3! zE8;gtEThG%SY)V7Q?XtISI(o6(c*SCrTaA=Un;*H74S3kyFJR;dylxrjrnkQD~lo` zS&JeaaTMN?v73g2`u`C14`Kzr1~#J~^FT>a@S?(ZMIDXhdZdk`Rc~3N>O7@dwq8DT@!a}eF{zFDvjebBHoRT za6a3NwF25(-DS8fnj8%tXM;r}z*6~{y*hGxFn>R9EZivyxWQ418=;6Sk}?2CND2;~Q}aP+H!4481L52gXm2_Oh{{BS@5xjtu}qD8RQ zmJGhJTl0j)=_`U^x7y}`G&6Tw4W3S+45bW#&?)&lINFm*3*-e0${nMkL$nrt$k`OY zDl6pdLURKxK&jSGti&=aS9<6|l0f-`^HZ~mcz*DQ8@MKrmwS219qC!f=sC8Tu)c~j z%0BmK>!?qXeU?Oi)F;+aA5J@IUt^f&Ngc;S2Qvw|7cVt)v#itQlqz~UKgkSJi&26( zPuOwn2gh^0^RR;Iw$Ca^p;MTT`7BU;48l^IDB#B#^;p)_k9aS%5EGfL?jKd>gy1I% z0c9bj%jkw(b_{cc%_%#kj9CbN{+kp0Dmd37o8nds!$Yg5H2*;r>=6et*Vtn}y5qx% zj#hDHNf(4j9fx{R)a87$V9W*E`O#+Ep*h(>6ouJ&dglk7;QVw!&w%KlS9xmCTi6Lb z_q&aU20f`#1@y3XoaQTohXW&+KQe^c$||qu2G|u`wb~Kw9pypZ`AJpz4W$~vW|5^K zq$ueUN+rzG64l(>pc4F~5U%8pdWH(=YT^sZ6SHkG@X2!%GpD`p}kZur;=1+qhA&7J$+r0gpFvjI*4kVh* zkM=iZ_vAGMNNgkb*3iabzgivym zUA8lD`rr^ghkS`mF>Z44DXZRGQe@`q4)`%bI$P<5dpn%q%!kDa6b;#LA*DqN=&mxs zz!>{EC#Cd5^eiR6LN9kWzfC1BSavBfXh~O6v@YFzm>krVMFl2mBhFMlkkMB6UWqeZ ztIW@8@S5RqK`loNn5f5o@O1&|)p~N3BPJ~X8Vw#3z|aK^u}D9X1McCr{D0WcF{qC? z`4?n%=B8$JrtmrB{Td8qMBs5w30UrwfE0t(bO9#A;e&#k>X`<12BFB3Vs)S2zPSmF zg|%`l77QT@I}n%PmG$LQR0Ht{SB(JtRvkc**J*JEGB|oxe1A*l_n|Q628d9K;dOc< zV1XGYrr2iB1J)VruYPo3G~;Xyu24NhlPW+jGefTahrF!lP_hb*#B?EoI#r4$2545o zBKl&k(F*v5xK21LyZAvcUni~GaOQ++0jIs4aB|fgoLwD*v(@Q@^KQO(GeJvn%ak)3 z_CDD_^_pbbNe-UUAbsg)c5?`mJh%Ax<(=OWq~My=L1d>A`DVu1hOAsAyhM!v=ZW(+@=s&}!XVU?gTOyXTwvl?mCcUn<`AVf=G5m6`0>y-Hm7b;@)zRkjbhcULTk z{_NtbGVI;?Sz*}jj1Iijy&z-If5Zr~yJ;hPF0JB3$Zl2Q5w&IaT@}^J?OL3mnNx|O zUk?=bTXVAn4!-7&+}>-aX=vs=VWeT(M0@=Sv@8~{11pAtoh|(1s!9;Tsnj5Zj5^S- zrBqAIBfLQCZ<0mot1-NX z#a#B#yV`!F!zlz4d`M*orw4h8w>(56b9)il$vxQF3$aV)tc3i8RuB3%MRoEd1Lsh< z&JLxX1;b>au8cn5WLQA^r8;Wd%XFUFVAOy9@a1uq#cUE%1=e*eGm+ z?9c(}*uDYMWWx=_#ke)t-L(jNTKPA3y8~COx-v)Y{*S!TUEGq%kB z?##q8`?@nNPOG_~JJaHxxt+Z=Mh(fr+nbBJGkdMgf$q$TWiIZ{v_RqJlI~19=+Qi^ zJG0L|AMDQTx6H#kGVKseov03cBqd>4l=V+#X{KK^DbCm&amLd0@ENh-DC={DgI$G# zd11{J9@bTOSYB9lg-f~$m*j;+w5ZR;U4@JD!d_Q6&{a5)7xuWqMO}r9@7pkPUtEAUT@*Otl35QqKPt2d#2$A{_J%mJ9qa!P7oW8=bjYA-`*LhD za)sa&0Uv&f?n2}^6w5jPWD3WX`;roV9hG14NP}tRbGK&xwiq5+OY7ndj@96#utA6J z3@P_D+TlrEGRC~WAlp9E& zwoXyA(~cQAb*8 zsgb8nO4wU}cK9yIUkMis_i{LMgKqoR)HQBS zpt&kxXBFa8txkWd4vld6z8c9&+XakD>kmxVoo__h!kuc&ot57%g#aF`NZ2sD$v+#^ z3hSSj$z}B|I>s)z*4HjY#bc)#;v#pxppWwfSEbvR>dWlw9x;5r=cDW6h)Kf+o)^h! z9Zj{9rX;Mha`uOG#s%c{ZwA0mqsqSFxr2d6yo8^+F=#IYxCU!5%Yl2bfjbAVlMsS9 zE*Qa|j26&eBcSukf&g9M6+sZ>A+VP1pspS8w&SKzj%zHva7~w!*g@S;C{~*xgT*Oo z8m83@(^TIut;WoCFjW=Z!Bl=%mE)xxvf7@~x}lhTGt*uNEf#LCr(r2-#ZuG`eJL1v z=R)A6XCXMtsI8@rl^{GwIdTa?gp|N56!KGs?wNH3cS;Yx)MkPoyTFz<%DxuE(4y@7 zFL=q)rXt_J2Vc-L7n}pmq~-6F?si0mv8Tk&=!~VDjaE&(fJEW#V7>7_ zm#`-Px9x*e8wdwtV2}j_Iyg+QpUaG3TG`GWGor#QxeRWg#yJ!WOmK(mkzn^o&>4y; z99;9FTJM7gVMy)%<%`6P|xrcCmE^ zL)FA(0=a)mF5`j+gG_9Y;WRivsXCyLXaj|W0}9UM{eY6}2NW2afRa|3cW*Hxj;;tI zc3o**C;eLxsT2_5yIu!}O7WF)=E4DGz6%a~)`A0N`@^BP4F?S35`-&6aMMi=HeHLw zoPz_^{iX}Au<0h9(kbu2hmyUXlR~essznM?Dp+`_AfS{j zJmYXHn3F<50QzmpoB&k4weZxpRdt`WAW_Utkf^#U-H_-e0N7GKf79pK<8<@avCA_Q z%_U|)qK)??x|l)8d5IP~0G#`w0)PMpB^ZzKoVb{c_GOG6So5MejRl8)n;kG2w#nu& zfw+Ky)yG@ln1FsAwao;qOuzVH2&1w#Hn649Mt+z8=^O?O-{F7`xN#q>RNOfz@k{f^ z{ZZl|+_8%nw76q|62DrW*hUGMdkzlh3xlAQMCeGBH4GOk$o#Wg0wD`>4CtD06C**3 zTh6{jfzv)e1hCWS^ zKRccM8VCKg863=JxBmQZZELX^$bF(t488R`#{Ol~3^&P+I&JB!>8+luA=^Eme_XGG zXXDaIWA+HrQ1@*F4G2X)xkE$vmS_x&#q~5m1=W-UKk~9H+Kz9z-V9D#s@$aw=@&j- zr1C6dQ8*CjZ+34!MchC(_3US%CQ&&96)g)dYkHS)uT!YYxqSH@G(*-8ZH4T+|Bb0N zJxcD?n>jC$iA~OXillR#}Lw85GsQNXiFr)2JE0(k4L z1eq+jl7?q!_$&+K;Du`;8ovFkhv5Jia%EcjEw1O1lL7JrX|x|3qCQ4^4b}RBJBXwl zE_VnvAS1k4XoZ^71QU?henB^GsD&18KuX|-DaY=f1q7Lc{{AD7`_OgGn?fInkbw$Z z=b9`!C_~M1l#;_X+vm`$;WM%}S~GO;c(xgC7yT(8XqZw=!R6a?Bo4Vc9GRdTCS-^$ za%l~|0V@Oyrm^c$Wk76)=%G1?Z3tvG1}&KznT=8BW`*hnz5{0=hy%5uZkW|&fLlC~ zftxpo42|Dl&3NU`FB%n@*>U7zzTTYqvS`dVY0noAkc9y7g6aj79D1w_=II*>ZZpfW@1sEyQ!Izws&$F!-~E<&-a7FtUtw$QqmHimb{ z;s~vCRs`(kpf$EWKx(12h=xrJm^DKz{xQFT6Zz;8Bp-0hsOf1^Iy%wN50G+oz)n2O zJ9l2O9r;Z%Z2f-Eu zgb`cv>|_z_!5Z--PB_0o3}yC^D3SLvedrxrVG`mg539sI#9C-K4bGI4L6gfK%;zBn zdn;1D;X!%6;DM*oz|u^nblm|tWuqq6E_*cpiXm!XXD`)&lxc(H;481$6Py&|92ku{ z7IB;2;4a*6uQyF6yN@?xPf9$XgqBhXJzFIYDsg{aqGzkx!%FPSOZ042dsK;s@)A8; z)qbYLBYBCQt!g`eLE^EzM9)^WYgF=hUZQ8K+O;aVnm-z~o~>%TI!mrsLOH5-U1wsq z63S7@4V{TSN+?GqH+Cj&Q9?N?xw$iOn-a=V$*rA( zQON^J+?yx#Y?VBy#Qk}Ro~>#RE3q#x(X&Qk^lVkTRwY;SM^n(VRqc8ucI72{wyNz`;<~&<&sMcPO5Bi_=-H}vixM~H zC3?21-KNCNd5NB_YI~KqH80V#RqY-nZqG~fY*iD*!dLKyBN-<3635GHx zsR=PpPZ>+2n)rIIbs#mTbS^c@9?_?4wu>UgQqbGZ;IrWD8Kfh0U&X7PZzcGb=~}eV zaey`(P$#K>hPVo^FhdRrK_d-MJO+khoP-~HknqYGiGrCCOY^jj%yA!we5?T*FLD}` zV@W8CT}wx3dexVso+{Fq^_m z?phlnAuwztAq1zg+nH{DE+RBwr@a`uPs085k;D|zNoTV>AZ1r3k%>g1Q>H0(J;mZe zT~t&BYCJkTsw+u$l!`ep8#Ti+nv?}-lCtGaDNBq7}FzIT>uu%QA!6q0WG7+jS0LAI_etj=v;B(uF!`ZKD4fC1>ImBdgIDK0thvHwA zQ)nfz;FZjUmrP^?bXzN7+6FJ~F>Gliv0YtFTVxehvozDTR$^{jfrecI9=STAhc2C9 zcg)lYC8wJv7DeL)1V96HD$%&W+@3R+3Exv1Pb^<){hUv@})|^deyAn z=&CUNc$;JTL4T^Km4zqk%|;-}E&ohw1lVXcDoWj0SavrU=BuiSmffb&o=eWR(6Ms9 zB?>LHDG&wJU__2#2^3v(o|j?4jEp7P47*wbY}SM(>#W+sWNAY~GX-b@ad%w}17)5e zXv;#zpOj&6zZh&27;_JhTRMZcD$kkjAuOFX{Tx%6aAa_wK&wY`1$KD=D}?2f9U+a1L#U+j+ZO~wH_v}zM_$zNu21103!i;IIbBgcLmgbPkYXj-3NM&JA$L z4G_esW%nLnfNA%asXL9v0GB_l1LRC$=Kzopx-woa9b4o4&Ot4QzLfI=?_RqR#fQvkgkr5?FA~cluRV8r8uuP_ zVhDTLRuR^hUujGQ^RTTRSed{8hL*z_W#Hg&rh*Y5u+lHmmD~@)z#q9pvoCw3HCxcf zZEqONgbs9SFmns11DS5RKn^XIRjIO6f+GsHSb|#@-ABiC9e_*Oh2qM z-1_Ean-!e*tY%*ldQnjJQR4eJ@0fjbA1qKe`!V7hSg|az->=(-<65D}M%xYUo@sgM56J%7jvr@Txlg+nRur*9>dpQm9!9DUi{YB{uFaBM33 zOh;020^GKh_MbKbW$8OYdgd&(<@?PFM@>&-C+LLJwo|pdSFu0(DGF^bBTtXA@F+H) z1S}xkFYBRSFz-LxE)eL+QB3!5i)M8P1(!i7L=JF+7&Mx(SGtsoVT3al9KkZu0zW&^ zV&L2@W=q>3VoB80lrVq7)5fXZv^l8&2A}ujvTVZ)0a5H@gCc6j?r1S8#&hF{-|nSr zwO&?rwGZ*1;6Aem9X#V)k0e$OpK2kr36{=pnTz{2XB=g=)1+>q#=QMT7GXHs2k_kw zXD-0N12GqfrSTCLL;$dIW(3o^Pc^h~Zk92JYDF@w)X zA?|XP>h;yx6};{&pRrULo8#Gzlmt{n0xr)2s74%MHfpy*LIuxU;6=Jlhvnr!%1bb7 ziMK1(7FwXPi8&cg_@B$7l5Ti|ALJtDzzq!om$BWvZCa-&J!eKOjBV4J11aCn$7YJ} zjN7znO{^xLqfUtG6ivrf*$hE?wb>*LO!Gyo*(gv&+&_y9p;A@eVa}vz(RIlHhYI)& z2#v{kxS4=1W#l)er2MjL#5Y_=AfEmIviJVMc2(D%=eg(H_wM_xt6w04Y|guuCz69A zB_W6aGuBblw%p)0GsR5NRh}CDa7|H?;sT;}(N#8mEcd zQz6szwCu(U^*LNDXjgQpbt?8SStKXA8(h z>XX%x87+vkW=8ISm>k6SLn6N48*lv#Zy9*#J3j{3MLee0deju7V`c zc$ica0)_!=DlQNZf?ngZ7Pn?z(hm}r0H~%_mswsCj1$NU!2~>0AZ8)Em)6|+o@GR9 zMD4|2`QhGH3EFpwzJnP;U$*Ob1zzwlJZy?yKwk?bVqEbNe#C>!0aAF5-epU zoUN%$RPC>4sg3Ont9%BKz=Jij$UYm(HS6U)4@rftU-4>}oQWYtwkSXx+=@cV0r}pZt}~o?hJzpJ12o*k$wo3D z;BNAPVp&>xqvlyc1Om4nB4@Y~1OuCp65x{-nul7(1YP@RU@5P8M&%;D z0oew#k>X}Kj-?(BT;0G2SCh&@FIa-XTS6~P5^t(3B6LgnRX9|Yfd&o(QbtxKX^}AC z)+05-hqWq@*~3|hWSksx`f^2Ul}HoTKBwf!$C(?k>#kSD~5QhEM^nO$* z>|DjMIYdBph-?y(t`~2S0Vp&C;@LL^A3^SvNpeS3C0=oQWtDWNXkY4W9?XyhFOv>i_=kWNg(pw5PXo? zVw9(+<)<9jDjqZq5p|oAwoCNmbP^`WiexGVxQiGpGp-eG%^OjTEjDP+s(Pqm5{tWwiv=R0$pCRpW~k*;24-RN@Ji{SF#-j4lNj zzEh2JuINkgz1EB}2&5gwRD`WEN|4dpM$9J_HMGl_4*NQ97J??!9~qD?ve+e% zGh0rZOEp`094l^)JuAf-z*A@pTnHn-h^$Gx%N{1%4*;Q~2nA+AcZr*#zA%$B0Ncf+ zn(8nEq|^nhi^?pf z3W00^1!6^aD;l18!r8>Ptndpb=n@y38kTf|uG~UKu(cHeSA2r%d7q$in%8-u2ZL`} z8lRxFZob9u#4|cC=$tKyy3<3S$YhTy2V=v3I~dH&@V_bl&2{c|J{P*(`QCXdK32Ux zr^@CK;nSImK0$d9pey`gbs5s;^c@|F!?4n?N@{NJz5F!eY>zcQj*m6I_*fIWw94X< zmsxRJtOPhXlwfbX*UUxZd`^)tl_M##D3PfRy1|y+n{wKd;d3Xbu@PMvG@0>=IOu%X z0tw&F6}Rn0Ow}6rVLt6VH2fpzmN3(JstSaB+qt?Cu1xF%9WxT)ir!e=pnFk8GU14D zgO@U9YWE~85qek{NmycGWJyPcu*4J4vld5$LAMy8j32YPfEUaZ0wOV$3wv9n zq3YEAg9vltde6&H>CAr1kO0C*`!TckNyj*T6EYi>dD892ngX#PM3HPOZuMb|DX z4~4e7C3AK>#Z+(=iya?|LPcXOQ;@IMk{YRcG6YMW^g`qssVT@;E2SN; z2iFOfyyG43cj6gyhycD?`Jakj`mf+q`z7D>t?u2<=Yq!zy>+_(`0)(C@>U^{c0=>y z3oMiv%=zZoe<9}YVo-rpVM<;Pqp^egq;wP0ppzM9VTnaql3j7W*?|uOEMZ<3<)<8u zHnJvs*a9LiX)o#tXRaJ=)wV8U6?j4}fjaR@5P}nLv$QRIi(`onb$ku2;@F0piVhn| z)|*Lcmjr5dp9Z+3RLwOPzHO&Zwmv83@N4K_#G$(LS`py9UDlfq)m!s<$}5Vq0q=H% z&xX%i@l0Fw9WJh7nY8=QkL(a38?iteEPrz<4s~)MMU5g)Ok>8z7o*vN8B%ln1NIqdhi0(CIV91>y3wT=F#d2 zgp@W-AQbEjNvfO9^7BT)5V92vyo`<;6ATCD)Q2QsE4M3zW~lXF0!;u6JgTOTJFnMO zo4wBiiKiqZ?s$G#yur!wQCw)f`C9V?!$50UX#6mX5?#udYMS40|c|}or^~iR7 zXl}g{1}E*r9RPUvyAE#M$ujZW4vm|R#aV$lObdFIHv*iWP~V*2HbHW}*>s4M{-i<@ zf2*-uikPprc#KGuggF(1eFa7(jeef295H%rGrv8L%%^4RzD;PtM~eSJ@osUEM~StdwOKJ zE%=VkZL#@*L%Vv#K7HrOa*NcaA)autSRn58bSIFH9g4`U=~(D=PC9N`Y|BePW%33~ zxDfK#f;NgqjzBZy;<^+}K%0o^XcJ-Xy+UA6g*3qhDWG6?OrE^p0QW-QxEMPJMg8}K zOI-G{WB^rmmv4|zd9L|jk%2ka{FU!qo}oEc&+Y2{z`ou)P!7=3k8oiQ$fG;Ut6;FW z97OJ$OBL_X9jM~nl{{q)PN=9|@$Oj9H<4nkaL}f|q&-Pe<+}w{y#wYJnMsPE{B)=( zb{9uROR`J5M2^%HFcN#4XH^al2n>Isg!DOPTN<}FEh}gM)XT;5=9-@+7mADLJ=Ye` z^Y9Q5;Z5hIaH9nkqJ%FzTcsPz@rtXMEPsmodud9y@IKgG)TrC_0{uceK&*cVwi&U)q@>xEZ#B0 z98Vku=D`Fc^oeB`ol2#!sTh6uVkP;A4`64>0o&#|OrO zR#gusZ6>S>)Q>|snrs%{!P+bwIo#~U+v1X1y9`_m}le&z>Z62)qK zzR*J@pQVDuja7>v&5pu$APAo?-jQGV0Y8Fbt|WM^d7844#RWV%%g6Sa@8_4hKH)a| zw8^0`(V%{Nhx^~#%%naSv2%c=s!VShyc#r^a-OV|ih(Z4Qv&LIlhIH5F%&nly>ss< zGs->pd@Fjaw5Kl_-X%wA_BIIhT=RW8t-g|r zDbn=Zp5o>!dKarDJqx5*^*3oAJBuF#Fx^v8r#WMfQ;~6z{YTAxpLCQuL$QrsTKoep z9B|jW?cT*#6#qS+mM<@E zlFd*HIa(GOQJql6Du+ofvQN(`B{y#e`vn(0r&=A{XsdqV^>CQ)Z8`cmxo(usb*Vv9 znu4sCdu#Dgc&n37{XBN>oU<^)_i4(AM?Y=#O_8gsa+F)aT}&yyIEM`~Lf@A~PuO-c zX%&mb;$L4}ZjN${$8hwol(SEWEsB^ne!lMS5`r^*mTTH+3o%~H{!)(8kVN}PqJ43; zZ(yFTasr-`=mPfNg%}x(Dtr(`eh16$Ld7bB$m)W?)E2`PSr0662y&T2L*CzA^eG3B zN`VpMxC*pbXV(^6RBbUFaHL&z1tHm0NpvBlhAp>fv}A7|G#>-IrQi8ERviA(IooK6`Di8`RnIC$ZP=Ax>>kqq#nQLgF&#| ztX}3|0lD-ciRWPK(G+gbu}EB&LRG3e#b|MChHz&X)>8#W2z$ujY2S-(vFEXb#GdC^ zScnRY)S4YH`VOt@#46)UrB!CXcL4MuuQE|K(;;MtNKf)|7tXVswrsDPwroEb%XY`~ zL2(pJU$!j)T~|&-)pqWd&9%dc1W@}F3yiHF3%6;_!oh&7T{vlv*}B-==2wRX)z>YbM`>?Q^m8s&W29G2b^D}TtRUz%?cU@>At7} zC=D7_BBVwj0ii5`sLv#MB43&w$e$2;AYB{8K*A{wE>gZ*twYH-3EF92Ni{Fa3#t@5 zxXf^vVRk|@C3+l7ilwdt6wz4E}LR3bdY%g32l@7%^?3TX6L& zHKKWI@^q-hA8_LGy)6thK$*mM%1b~v{d=wNZD}DDvfz7Ld&ng)ROhdK-kn4K{Jwxb zIPeHfiSB=V`iKpHki9Fc6{{RtQkW{Ok&Y+U(7DCwHkyT= zlK$s9XK&-~NgAQf*$uqoWV>?e%tg&qqqwbe7U`a!>ZG);cG1|r>d_ut9+knA2-<;s z&-K`JRr4G-Qk}D>$t9lFPaK|PbLQ|2FQ%ognp3FKD)}%a$kWoU^7_$vI-aVU%R{}T z3&b9Aki&Ag&Y2t)IQOU2IWsy_{!N}t229&(0Oo{%fDU0K4gfwW|LkP=C^!MP0Z88} zw1M_uT}?A3zze+42sLu5l2r*cJZL6R@2dn(SULoo+=+4=p8#+L*t^iZ$%=Vkr0OKT zU|isAAqt$qE#v}cdc7@hmK$bHyQ=Fu1N5SQI$x5W1eG^aSh20LBH*s8o5s?)>J5PeDcQ z)z1v~q;ByOg*5U{QQ!=SP82vBB4)lzL`wb@L{dt>9p$}hnMP0x<7EONu=j+(HCf;c znM6tE*&!no*oAATqQ2ZHmjrI~^9_BO^N9hvp6ZQ!s_^j zpE!`$P80$nwDp=XVXc|!NWfupr8p6}k6o;2T%-uOg^(|V8-BQ-GI6pUSz&Q6wS%Cv z$>l`Sal=SCuxxF$N)5AM0}{WR} z0#n@O#X-KoLtV)QUg)y;c1irEdb4m%5+`7~>i9;5S-qzUv-Z=ms3BIBZ?LM~TY=IK z;A(6W8d%Q92Ih=|mQt7>)*gPEkK$`TuPZ-2rJr!O_SR=_Q=P!yAM^F{;!!x1O5%u1 zVU18~MCBq}) zm>uB$1KbyX%Et%u55OE=fCJ=f9Caek&*=)5GeI=lF}pK2$pUp-%#)j_4Md5iR2yha zpELvQ0F27x-(C*pJ)KQId*I7(NbO@dBvC_%z9E8zoVjx%)%{C7}~i<@k5GA zG~^#e%`L6>Vw|RM8{PnHV-MvsTvcMmOg@8IEt?s|SH5uD+6gDq=;P28rz0*L27h10 zf!QDauS!2XBmE~51pKZuKx5J3 zWG~f+0g)0+C+fqz2S9==^<7IMg@+8{#hXCdG=~1?qi$r9@W@!}Vb#^o)TB zNkIqqkkGA*ErK^XIU|dS2d46%DR#}sjL)<}J3&#-ZG9MbO3bUaEcIc)K;{=4yvTKi z!~~z4S~00N&H?q7YsIiSMGZK20j*B07{D1A*|fD{j;*N`^K-k(<=e|2LrE&tb1BvH znty9m&+)zcmp3#$5|HaM991&3FdC&^=r<&45o{>LV4h?t+4?|AOd&|^Gx9NkyTU*7 zb-mu^&HQief4$8c$G3)Xy8}Gc4zH|k+|>RzlRj0x_T$@TIjJ**{^2nGE6K(PRGfjr zVz7i3Tv|d*lS9sI01aHGfIarFAhP2KWV$R7h55Jq*14K7xQBea4USP<}ww>`*B3pjvtcbG7V! z4SbRZjchhI{An2`(}-&gdj})7(6x4iEW*?getFsmXQqxYCgTyt1Ct~C6GjN@J09WQ zwIkfC5niGZGLu-D4h&(gr;hNm(?$rNd}bKN14Ml#AO{q`G6u3A2ndItlg1BAJRbl4 z!)qYCU*o?@`XDb)u7uOPZs)nhb;z@oKsD3Cr7c|yM7*ACVyC*gcxn@1HTDOD zv_b^}$5@Q2ZnBNb)W(%l+aLimwrn+l02(U+C^%X~V1!SIgG0)BmOAc6!vocSy^J}O z*>K}=Y_vfn>fwF1Etb7PwncE7eZS!Ves%F*@#8*hpx)1av$&45o#I$G9C*Wt`*bq! z=)P(wa7ZT#YayK|drV?6G9e?-jdpNpVB8sgm2tS{H@;gt*$pFlej^+wUl^R;kSUtk zfTrPn;XI0X4~oCzOysr?x!Gmt0N+fnr#??@#EIuYkoXg}_y&Mxsqkh~umbpg^hN4ILLxt#hJ6O~wHkwdkYwTCdNC(M{5Bcn7yS-d2P3f9f2*zC7 z(?ayL(DtO0P5|z9G)YZS5Q2zZT6&XK=BW0~JY~vA$4$Ht&_*k;OC{_!=x1c!N_k1j z1`=ulcMLR~Z0DHQ{di9;NyyfmAg^V!HNEoux~^|k+qdZjv`h)Lhu>G1yAkDJnUqjT zPdp}A@%uNJmts}RY`rd3r*gEwl7@}0qPu!t6~=o=U1Wuk-mrSw1Eb(NZCK!98)-^z zT}AMQ58GFR*#f(5xX!TTyLfFMmQ4t%c_UmmX!D99*semVSd@4J7Cl%ePN@a;R%i%X z0bvthw4~KJeL39BhBQQ<8jvT}0rG^$d#3=ojzX~`Q1eIOtA)1;JPm@n8$ww)xgf~D z*rU{nv*2XX(VHM0{|#HQLpm67zud2XuN?Vc4gg8iKg+5)&1V{PBUYb*d0v_sOsbF{UbNa|eGe5jEe6P9o zyF{?e(&AKS4*72J(nvE}=ot}0r%*xtB+BTEeulQ#!xxsC7pz4}ffdB7v1XanUGZ>8Mr0aJ!7 zD$1zG)rI@1S-U1!R^bt`yOIs1aNwlr5AR{uNuLdLQP07hnel6fg5R=!K0;M;MUdx- zwack|Y`2QOSIw1*yvtLva|#px4{TO&%iA zu_Qd*hO0`a+pmKkbQxqRUO>+X$(23K(~_(!M*mi+@3GoyA=uwqCEs9+Rt=Oh-H_#m z8;Du0{YGvumTlWR@|3Y;+RCqpUUwnoUKZGcT})O?EH@JGuj5&fcolVMn|RtcItDkK zoS}<1@N5Tf(4=z%)6f|aXZdU~2GF$_v-ILH1|6y}Ffaz!^0UVnd2>w1n822dVOneH z=bCqW-)y%irtR%*3@Cx|mVL#~_p~*4K{Mn=Zby`=`6n!wz2Zg3C&_})T3T+>ba>S2 zVT;Oa>jU%(Dz%ox9fH6mHo*HSNpur3o0Ha=*Qm7XwC*|qIU$kDb`(Xw&;Qo`*S$3( z8CPRUd;uFSOIdhX`}B~kKY4sW1t8Mz@p;bsv(4A#ldE$WbBf;)@=U&Sm*2VD?=b1{ z*?)4~qbL37y?Rv98D;Y%kCY8!aUyr-y14Vu_>PWcitWOgs(*QiKhnm9t<22N&zF=xPqU5#<8gP;X>NU&-3~epcQ}#62koj(CDsU4J7S*r3Wz>eSSDt( zr}sWsL~$~|?i#MU3s|TbXY~~apD3>c6|py>ylw=IcA~tQA<~l{f%VLf`J*38crIzp zzd}Z<;s3z_#SkGxWy@jQzYg~I8}<)O4XyPw! zmt(>i@AA+XtveTBM&a7EYqc6MCt~>FOkH!?1>e&_B2_i`oGA08arMya_G3^tKPJ*?1*l?gm7{uyxA60MJ z`Ah3qdQOWqL%cyle7lC|{h#N8cgC6ruh)Zb6Cz0fdkT!ye#Vpo+b&ZMSN1uWE-P3c z#P+D%Xlx+aA0yY|927SHI>&C&=bGqpWZ3&4yzPO&&X7duBqb1vqRv9DG~!Eq;{cVA z%Q%#(hNwXjI7itmjt22}GmGJ|*xylCGxAv# zXD+8aeVGHcx+-r3mo3CwgX^ea$v+j}d0{kbWsMwQm0d(s5pe4*g#KB@XJ9~n(ZDUQ9pAqvNlj8?Cj_;k7aN=$B@vfbaCnNKwxOK30kM+lAc5q&-lEy?#AQ0hq3( zw%{Vkdp08P@iu4&+*_16+P)lK1k%wQc6M6U31|T?>{l!fSi)m8#b@|g)Gs+)muC5% z)RP*|-q9i%`hrp+py_2!=RmGnfYj&lz)}p*x&J~t+wz4$7AjwO)!X($SQGpG_SqFKdd^C}gDI}6dGKF}S zeJq`X>y!H>h>;><09DF`XV8y<53(7M{WhfNx=PY^b zb2Lrb^E%dH7QOkk?W*DTYSV+7x;QkaeU?+d^DJk1JWFkMJ-bCwCn@Nq6BUEiU?M06 zh0sJ$3<@E|;3P>O?YlqKbrEAiF9iwGpB?(MQ-3aIyIulS-qYKzKd)g!fP9LTi4|=( z9R@M&t{2*N(6Hp5)NVTy%N;>1>*i_lZ8Zw)s<9{%w@RZ-SV;@G)Cf!Ga@z2HtOE|+ z!;3YabJ=uSWrT0H4q?^qFf}>*%<_=t3@wfw%ojOy8-7Ftkzus|)*pbogslA=#TEuFKP zrw|*Pw)Windd8&e&oOZ&v!TiZt!<^SPFPnxvbC# z`>l)ju_66w$G&oh4%2gvvSdKxh$1LTNmq3 zwVb2;X(tJn^eL7`i-5T9O1L1fd7~$lZ}87Z3Iq=zcB;c{{^IM)Ew@*4(|9~g5hY}E zB0>CFP^}Zad@R#n!$1lyzPH(l`$$`q{o~($ob>Mp6($m2EfVdpDjVMYkYlj zbRe12NZ}kVVKOQ#Q~3o{gi%}GQ!Jx1azeNnU4}S;kZEX%fUy*BwCV3IzlPXKdF&34 z-ZJVdI&hPG8ke}>_BqFeO4)rbxFR7<%Ue@1F~7xfEpdH<(UH&HeDXLtaFhI^kb@l^ zxS@>>+|YbEM+Y*DJ+N1r*6K=#+_T7^Mjep}Xes zagd;b5ZZV1WYNLZdN3*H$q25wtTb0@Dl5%{e-)urD{CfW;a=UxdlKVlehFcfi?Zx+ z>91}%7h{j6h|E;OLtThP48+oxHVRaG+n^fOn-y)wZ*7?1=CFC7Ad%{VJT|6;ixF+M zM6}tkjWz=yBLG4)l!Bs&hEh=a{bm%7bCs6L~69w55)BQ zcVJm^H15EW%>zXyC)A|XUFEg=s{I1XKqesfd7VO7`OY4mug?RY(lIlr&-06W)#H9t z+|e{AI`hE8~-yRYfLf;RenII>HnwZZNA@ z>v;&k#(LrKh-?uyG{dtFnu|3O>FD$xL*Fqve_w?+w`6l;5DyE@P)oqun5$X_=exFed^iwVcSbIu%@0!6`Myj@WSEL+olD9zXl}sUuK` zVI=wF#3RITL59XPA%;?&F9_VSb4pBK@F(<>A|DST>zaNLVR*l^ukZgug&W}0UmZBf z#)aL3YGMRzFrC;ds0vFhwxe;3p-ft~>ZQ5bXn23PAH#9UteavCuK{1H<}q|bDa3F; zyej>q91@}?OyMf=^vTp6W?5`u-L{E7VCQKzTu$_s&PM_Pbg15X4UuQ7a&3frcrdt! zg}&_kaJ$E=D4A0U)uAnd3(dxk4#=oo49KF1kc^5bvfQtz7qqkLgbHfOyzf2X8bTH$ zlIkkmPt6e;2xomD%n$V-;V?|FXKW!I=0P~+?T|m zK7E6!3Guj*_ZT;1>DLI_Cltk}fpkH8FM#6NE7<_IaXnvtD!t}QAJ$Ge8vWiB_ut;$ zzd?3x=16KQS}ceEU2M&{=3Pd1@O*QG0;~zF+otEx3I_t;OHD*6*O;nYF@i$Aj3V_}Dz@wPw=<+`5wo@!O!fiRn)Zs>{=Fp29~h@pCNw zXG#(Ym1O~20RBNe?kF31u}Sz=5f(#3vlJNCPg0duK{l-wbJbe0%myPCu~i#Wp#{J5 ztH1O5tKoH+jeo_ehT<2J)KKW5(NWF^sd(pTuscuaiY@cFFoY#Mrc=X+ehe!AQ+(T94CQ=4wzf2Z zZN##Wc!5(}@9_;byAvIU52Nz7&w-Fnt7C*K&L~2PKcY>Dvno!e zm&*R_73NgyN$25>3wLlbeSeRxjQO5ki=Z4$5cM`XP7b9A{W3I?gyM{l0f9 zk*!3>3FmO-Z5eQI_E>N11?E6rOtZExM|hX`999N!mfn-%6yvdvfP(oMZ+$=0nr``x z#7jg)>Zvx}621z^=e8O<#O@KD+910?qp_d{s;ZK(pSL&v7c1KA82*~DADw85H_mjyRJ0AZ1#)B_^F89fN<;)2hT`@PU&ya8Y-s z1(qohe~!>oGB=)v=nIoe&V43TiEQ2_+QAi)@9)r`7u8E_mT~yn!&OFsQ_>*@XHScd zw)e?V(YgN+_a}BB#~7+xIj=$*8`4dnMaM9Zl1FdxIk8w5lVNNxzgKZw?zI=a<}fyR z&lhW>qnErV+dZubV%Z@_YvLrfIj?PFqN|RQO zR;_ZhYQB<{qYOjJQK=K0CXNhheP(ONqM6V^;ab#Dj#^}wdy+RZPbz^&&Y%)`=?!VMj2F>T5{~(hB6ze7TNj6Iq{MX82iMXiGkY|@uvwA_-`=nfWD%29r6m7}@725A?;hD;w9(TdKm&7lTB zcEmbsxmk5BH$+(AB!fZ+L?7{T51MyRhmm0-Uh?HFo#&WlIL^hzC|}Ef)-Br)#j<_j zt7o-ry;OvCD3UgCZS5+GA z>{wkT=A7@Xx~kH;WUGSCLok`|S(Ns1eBi@YSK&(j#i=8Zx@s1U3OkgeSeydKtz1hz zK{>%VsgsZ0v_JE)yPuyxp1-;3s*TFgS=C%nSG@|Z3eZV5v%1ReR#Vkgc0vqw727G; z5k-muK{F&H8oVCZNL?jHr&U}Sk|SxbT3RQgn7kzQF1$5V52$KHdm)4Ael*9hT1{0I znkxK3r)jE0PShexH>s%_XRVT^N*tpfTd%3&Kv=7(YQZ8fO=zk%T2qCz8k#BvMo4+L zPE+L*dX1(k-f1Q@Rh!&SGoh*4tb4W*q)#BepL5AFw_^W?)CWnOuzj?eDx|X0XsRSP zWK9)gmZl1edq-2HK79jlT2rOW%Iytlsx*T3hBQ?gNEfsgc*mM50_&uy`lF9zO%?r0 z`@wx_s^b0)Vx%Y3R47E7R!xwfrP>e}S zMVH4FdYbh{#~e~_> z9T~>orM8mF=LGzg(EJ1vp%(iGVzJA{`kwi&zDK19LGmqu>q~*_b8%Yr(OOWkzE_uR zCZyX&Ss8Q8x21KkHin5k23^$Yj21?e);cSN*GU-zO?iYDfEbW&Uq$(5J;{8%@xIkE zPR2G4)$W#~0lLtIg#uqFMBaMSeZya;b@6x>U&jMwZwjGVuUg6QXK;W72*xBuWu={k zI#NOIv+u13?>$ie+hd!5eEl-@4*5!HVVNQgUlr8L%P_}^3N$Zl; zRE{-h)l=^V`(?F<|)?*E}2#q3<(OBlj!_XKm4oy4zfgV~LK&RX#t=5Jm)4Rrfg(tHk z;83(dh!C-8h~7poNpHg*k5Ovc-TZ+CNFYs<%b z@(-G*V7I9KS2a@Uco^)+V8I}b=T5EDgS1)UV)%;Do9xWWqiQdQMqgwBCVcp5C^t-_ z=zGCE89MlZ!DT@llQ5u5iI@6OQb7@X(yU5rq!}~YSUlE{)hI={c}aWB4mlq{s?9)`&r!QN^K_;`P0#X+uDQ zZsioUm$a+HemZ;g*ejv9!lzY=E1f-3T!COfFh~qzI~Pi-)Pqof9ClcYnJqzU_@~IG~GNK~-X{rQ)4Vh*{&* z5VOL-NU}iMNV#A61d^#*z612;!3whYY916H9kJ^uaX#A9tg%jr&*Xa5h^?scR4Uvm z%F4IPcxIO0+3bAr9`a8Nl^m9OlWj#(OSYzYcHX@z-n_~*LbZO?l&2<+kH={d4kR8| zvsYQWMfdtq#Tdhq!c~V?UzYjdU)srwhncS8uJqu7=z{&W3aP}@hk^#V%r#<}p*ut~ z!k;zOq2`+`;?AN_(Xy)f+V~4)9~7FznPMc+w<{aEPH#h$2ry}W;ghF|-8pB;-F#89 z6q+JEmXZhfZ6O8UgfTFtpo_F3Nr#Agvr(m+BcM;jZ8x`so=*YJ+W6W54Bwizno(q> z5kJ&$Xk2!T1W-Xk)d=%eT`0I7 z{H-Qk&h1ly3CY$Jm=F;Ml|!bJe-^b=FpYcLA2`YRiLapufuQ4~S;sLG({YS1%LYS_ zbEDhAl{)N~#O5DvZoTk7xmOzBE_n6bfhP#(-}?f zg#4EX3MdO{cy5(**1eh0;fwfdx4YAih*8+d0)Gr9Yo>W^jthdr*;A}u(SvJg$U?cI zwsyc2(0L70z_;y8h08X~Wh#KA0yffraGcROboE}gau)C(SEggR3-%A7YvsT(uO@eS z9XC&W@`GStd$0@Deo5u7$yaM0oex$1kj%}H?0hGw3fA8Ni}9gQ^4a9FVMf4S8^<*^ zd2KYwF+?O`jb8`@yHxuLz<`{RsAzCc@8A`^OW;YLlCGb%;tUw`cG`}Kx5G7zFT9p` zJN?8YDM;4xb_lA8S%bHOdidR|K&AHGtAq(x`X}C{qGx)SM>1a+V_3)Fd6C83(Ogom zfvT!w%Vj4KHQIu3cX=BM|NGQ)^`yJgz2u7CWk&PsjG?@Jy#^|wJ9e9-jmuVM$TKjp zb)}1!gAQBQTFiFq!h-PwOzBJ7x zhNJ9~%Hs5!^ON%db=$Bw>j7mT;Ia)uz)`tnqHY@@c`%k8=L4(@QZBcJp;Fy8%fwbg zLq<5EL;gD7o(X}Dg0m~OWw!(QAZRZ^N1LzWb|A+iVV?$J)vA3BQqeEXBk5H=3!n9^ z{f#}`^))axwy%NaRm6Kl7G^6inIaS{I&y)xUagE-x%1VJd-(vQ+hsF z1+@EB<`q!yNi%dx$|AJ=IXfMiq6kb_;;NVet5i|7f>uysfv0C#e8Rb#Up`P#hur>JW@!+mHuS z+h#roB;1QWO+=)Mt?`_>n;eb+K#ru=fG6f%)T){SEyGGm<{v4h;3?d0PsuL|NN8h) zy_TiCAJQZJ5BdrlPL%pxG+V9M!t#(FfFeHb;+3?1FJEK08aG(KLl5lYb;hAwh&aF5 zV#*4KC=uw6y30SRG$Qd9fCoBfFFM^x$!T_0^ttd#)Xhz zX@frZF)@eI7BMmuMY>6S2HGckAU24Kt35}$lRd&xIEiI4biS>1$G?uFF`La^M46j|ZIv4msU%ctO);C9 z2~g8qfqhuAfTXiz_7rw-9lzZ5TLR0qdd9+_cgZodTVf3`bOAao*>=!xJbmhqwni@i zkdEbLJzprnGn0J4+S*;*9^2;fadxSXN_4md*Rrc#p>G6^aR)vwB3J z@bj0Jw4=OYV#DWAraMM}X~@2Y<_luLPhy?SaLut)gPTV){r#1gsor7Ug6`f^@n$C! z5Xw*OUmo#q`u%kO=e@F-BZGcuf>uj<>t}X~RDJ)rsu#(wT$lE+Ri{`z$`M*l8G#^V zyuYV+SH&p|phte=t!@jR8SN|scM*pfn+rG_=9=>8Aq6!_-l7R+-) zvO6TMU}ltXn6QhBcTPtZ144i()FS7c+$yT+aBucGA&Bg33 zU_>Y6QD$}<8>e5OiXxSUHi2eDmxzgI`szr`fvA~hDkKwh+`CA&=yvKIgSq;S-gR5a ze|*!ubH}}3E|!ISb;JLGq+395D6cCC`^qf-+#ex>d_!^$t4~}dkX~UCh4+v2ToDNb zI>pIQAon3oWsH`w_4&_b*^20}l#u)wG%4h*yK^O)*bdFFwL6U?pyGwJ(}wRVHEs=v z`zqs;>`8Y20MnCT%uhLmFprFDCvsf;kWPb;1)cAe)2w=_tb=$`F$BGVO59PyK0!$+_t|rJS3~3km^D@w}iUL|oqoHy7-UFODcf3op zDOU%psZh|Nz+5ni53DBE6^l%^g+rFK&{EJa27Lg75+EYPP&_|e0DozAG&_8M>P*j4 zFsOgHE|q@?-d2O|_6Xak6^mY}hY7g~>q4;SY*2b<8UnczMIY3o9IEloQg@MG)Z*(H z$cyhy@kEO{PfnoDQ|JRAukC92c&42w#d7tR#|9NHi!xTrPCte@I(rkDqa zWm`R7pI`8wB#0Nv3Ns&p4+9-}!o)pXkVu>rMn`2gD~y~FyMYgb6!^e_9)S-$pe%n} zhO3-d3Vdj5#1?Q*|wS%XvfR*p4SR=6cP~ctVm$8tIQ{>8&JV& zF0)ZOezy74m!(kv6ze84p_VY$Ct^UB1#vISy7X=Aec5A=reLAeu+>E*p9z%Gy<)VI zR1j=m>unZJANW8bES_4(QW}8|>x#z%kk&E*R$a@Do(0}qZvEoF5{ROB{A_dQ zbQl>XBKt3I={(0)$I25W_1g&y983g0pe?j{idYVfb^w19q)kkezPEu7Dj&f2*715?lmzWya>Ulu=MO|F z@cHJ>&rThI0v}Y89D3wD9X^XymJ*FwE(I=LL=OI~J}lx5Iox!nKM1kSXF7VFtPdjw zy2|>nQ+(!!@3cG}L=@-%=0f1ZHV#Wb2O|)H5A)C(==qd99Z4}M93DMI>t>)A`9aivh*!>8zF%yoeOQR*su{KYKRX)fYaM0^?gA0rw zDdI+&1}A{xxzE$F0Jqr$f1V{d297V1rY}X=l<6x50;P|c2A2LbF%Ss7h`_1%HIV{^ zjfg2~PUeC;f(`;Wp_tZ+xw2Mm48#o)1M&883hdqrFB#{so*sf2996{7ubY4 zzR4#7oWdtU(_G?W-of7_H4EQjo#C;2-53b+Vmu%ezyHI+X)E|)G>rG0qbKzftULJQ zO@{EdJ_aI?U11RTjjkyQJ%}&}MHa|ni#k?mFC6l-JuqJBE%|K=^5c}E&<=&5Q~aiT zbqUDByTQW#V>+H9=E08NI=VD{U7+PH1ZG zVTH2`Lz@sH`5S<6Ar>-~X@mZj#s_GEc?TQ*TIAa2Yl)dkORS^}(0n}pvtvI<*h$FH z2rtS(=ibCr<6w(I2lzZJI$_XgP(gN$V(*BK}E3VfU;J zNubd3*|aTue3nDUqe!QY6*S#Q0$fx&kp$>sQzQYOj^)4rlq+o{0iQaN1boYp1bm!` zBoLIvL#8Uu>!S%c^HHNnVm}8-BBa@hXacmqC??8O#`&gH#rZ}yoY#vY&JbZaAF)VB ztl}TX5wQB1W!2JmYB(!bWFy@}3MB{!)||gXG80U7QXxqLr5et`GiUwNlp4-T$O~nv zXR`7Mb^*N|U4#xux~3S0YdE87vU;>-3vu5*0oj~DM5!*NJr6Y}+wI#MOMrSSYlsv} z0CF?GAO*#wYUo_V6{Oyv&)v8JR_7(g6`VT1oR7GIXE8XG2t#G8aT%Wfnq1D+i#qfm z{!OOpT7qFhF5AWrX~ktk*0B z=IV$y?A3G5g)O&ZO$Q_!qUQRqbQ~AWrBccife8{%vC+hl+)373v}&26`6sJa6vF9X zgv<;vpn{MhERxbu%J4Co#R09-q|tSb2Kfs#Wi*$+fTE-M{6(@)mA~MsFp>wG8$b~^ znn_=DIY^_TjXHFZBO8v6&&pyAa8C9{3tcc0VIwUn0al5Iq1l$vmxhVGVwbiML=F;P z0&G+Y3_ctp~K+*i<`$Bin0hI`c^$@%%*;wQnPG6=iO&v}<>|FYSKmol;Od;%5;K4*eV4PX*jGS_MBdYbhSzE6S6Q<&A) z0mfDa1Li4A?;XheKw!`lzdju@&~+F$Lk0+F_(c!&dX>&K)>|_xz{h5zZm{Oc>XU9>ko?9`QFG#V&mheyz{wPz+!`|-&YOt}3^r&&SH7_DHU2{HZe zR1}{ephEG}Y<*CC5;&%cXF%^ie{ec_OUXJ3oWHqP&I*gC^eHQ<%5hXQA1J=IDAE)Ol+GYoQ05DVry1Ffd|MZ>9oaG**#SD1dRUg^}DX+Oq78voh_cp{fupV>^{i1@wxjD z-`ayW9ik^e!!w?uro(m$Gze#&G|t)1+c#NdzFovibR7tM(?Pc~7SST>j%k^OW$ zCPu^CFCC%2g*c!iJ{_~V)iMyJrv4OLu;oOVE&-mwEF2u6MYM)WyaF$gv%Zv_S7#7j z{!i#59OUCYkPs`h%e6MY$F6O@fB2;t-OL!ai(arI8i!9(N*AUxovM5Jv?%|dBJ@86 z`fFNZt=i+c)(w`jy#WJNO5xw$fIITS8p-z`jy3le2w2`5V#|sHmnpM&(R|~*>-Jp? zSKHy)WZlhPyiem#%;(DogP=#573I&!idm)kq5xu-E6WOLjfGkS8;4YeV~OPCMMpoA=m^yWaBI z$zM0y$_um2UuQEe!;l$Q)B0BCuaglin{nIxb&q&1gXTXT`}cf0`vi6y!`y0fZeQu_ z#N@wxLuNnR{#!flQeaouaZ?o+xnU$b?$X)>b~(NTO#)A4mnkK%OLpA(wz@|$Y$tE_ zwBC5E%8rZH7PV5?aR<#O5{ynxhmm&HIh#I9Br`TG%35wSc`-`i1?|Mi|KFW>32VL_ za_cGyq?x!}d91x);;FRBOgnLmxOKfG6YLTf`zv6{*2Kxh91{m9N^Y2u<1^n3axqwA z@+?d$;DI`+0VJJl4|r1kLRMd8!~^8YhzH2Yps3`}%7}-380(x3 z>4Iv`h^LJnGXx0v0A&TWfyt+3!*IJlU9e`h`S~x0S$6#fg`CVy7=|%VC&Tb8UGigV z7*_H-z9T-BHcGjyOeZ6kl2Gz{*DySL;Nv_h0`bY|BamU3guiPI!)bzlyEEc#w&aeP zQHJ63WbEu1hEb)wRKqY0%93jx3D-4-VGIT@WEl3qTElQ^?lgvBY&$Q?Fr2HUL;;7S z4PfeUu#*KGP%x}bi-*)2*#ZSCUKksOG1WdL!>~=Z(qWz^E#5T4aJ(~_7B7N4nu)Y{ zTV;r~VK{w)%KJGRhLf?F9h3S{TDU9JO8lbN(E z6EYADOwE61i>_?H9B{JzBH(pwzsy9oUrs$d*1~VBYy16tkJ+kvi3N*Zb3uyFdhp{y z+mrg0GgT!GbVlN0+uC|%Y{Qh{@5gmT{5S?{Qi(H@*FFG0o$Xh4ewXvos_qD+-X2K3 z4P(1;n~6-Oz?*JpbwYNVeW4SN$N~$R^`X8vp6`<#LScfPIk3*XhW*u>UKav^!oHh%@ZndW1D8coYzekW>L-e5Vm z`Q5Kgm!-{QD=Uc9dSNTH3gAB{MaxH%qUC3%!{~3Xq9x(RC;roP-1rXFv+NT_&o*EE z({Goa<P`L#crI)d-&SzxZdt7rMHp56$T}txLWdFInsw!WSiXIbuNo*m`BV}{3$}yK1 z5?ALdDLL^2+P;cLTd53mBg`IBI+taySj@NLhxYH9oQrDZ2)^qoBdpIi$dBC)VT3=u zkxvTCP%y&{{Ekh91fZvy;_s&)O8ubQz2*%!;~!r&7u}4veAWEFy|>s+V)ke~bIYo8 zPO70QKVR}ZhP~ONxA?7Rd&;#T*ZC#xw2v#1#tq!ubQG=Yv&GHoscPT61&_mH#~vvN z00J(%i<^`cpIf~*Q?ZllljUK2sP6r};>{F7kcL&2qYR{KB!fWJ{EPSf#7|d8MtbWv zzw(D)`bS58^t}iE=hyt0OaV>r=$_s~U%KW{`lar7eyfh|Dj$3ATShDQy{UhDQxJIX zSHAhp>So*?@7#Sg6#(ztUH3@^#ZP-r+&5xO3Ro(SHh2FbK?REd3Cn7Ddr0RbmomMn z|2^?((#PKEL`gNag65I@UF$O!aRz)^0!}0h=F?RMgX)y>i0rsOq ztLg_JR-}n~dBfa5dkj(@3k`5Wvjux*rFm7QOv6RX* zM|GLEvW&}GJ>2k`pVhv&Rr{hTNrlE`tLEAVYL2%U{(e7nM#`6=+f8*81GPNanJZ;P z$y~_#&M9@X@&CPeGR^)Ft3uvscJ}Q&#R3qjVK0y50zx}L?*ieEl8wa!YC*}g^dxbh z)Kschj-$uZiqJb@Xk5U?0bzPr+nD85&IG{j(dy_1h(q2W4VbS64PX7}jjI)D!<0B6 zJ+ANk8GkLy~@O)cV9**lC0hSw-V z0kj+oVOTIs(wfZ$sS32$_z5c2_-EfrdMiF%6P21hiCK+{lAw$i+tpi^G5)frg4)b! zh$F3ojEux^X*n3OEG){Xt#BQovH{3=GzltW3>c|fX+HCxaK-+x(kg55NgDp154Jm( z2AYTRceUQ!_0{pyvlG86RQR#nmKSGL>4rNX_q?u!uCO>bJ6v0rIFABH*=D?2$ zw@ZLLY5dfarY003kCr{-_w;r$zDh@o5PZ#3SM+v<|NQe<%}C=r{Fy3dAAKrNRh^0S z_)~O52ogD-c^*&OTjh4*92});w}p!{H}JKBAmI9HXDqt`W43;&Gm*>m3(rJPIZ@Rs zITJShjF$Cp&1_2fDiLUHJY_@R?Y%t?LF}*p5QuU)DvDzy5DLA}`BNxuEC8yd1|wv3)QIk-PKo@}yMB0Y`|u?^ z47(D$qEwPJ<=mQpfE$Uog*<8QZ&leTO^`}xCK`I|vNywL$f=0Kybb{DzyGW48x`I{Qxh1kf@wCCKygqQ_;>oa)_@yzHxH z>75)S1K3l)B^LsCD$yuE&+^uTcx?_prTm(I6TtXQkIbd_ocKg$4_D4j7- z4Ig4SCwOu6FwyZ*^Qq0ft+$5{`*}MGCev$HHE2#K;8zWA5;m31(&5z$IZ~ZBHT?u2 zXNT8|s&S*K-Z`2l0}98jzRx#%b@0Lsay0kr^Kk0n04M!(A&y5*5F(wk!@E;@d4(hZ zZ}Qqu4a1){flrZ8XREms{kl@G)LXB`q`7*Nym@(?|GznP#K$!+)Ju8=EZIf}qsL#6|`e$mH)ADbN5!z2tvs$Xv0#nD;I8Tn5s82~wv5*oG#C8-o z8foV*9T{ysG`au>>P@RlmAW8De^VVE!7i$yw_K>Su|RBI7)APHmXQWt#sAM|9q0I! zc82w<4pC@*iN5Od4~@2@k!gfZKwEFo1fAh^Ax0^NcrhTO)S!!p>%oO1T0VH8k`ir6 zJf}0+={3P+vAO7-wVH`Omz!;eX?Lhqs+HxIFIx_c76pUU$s7x@5TH;ebM8b=a?uGc zAKiZRruvyvKN~vz>`*`3yZzKJuAerQkLPpRe)g&#yI|+}T>Rqt*>R@%u;{NM!esST zS|dD#M=TULYW96tpMQ82F-mJ6%RL}5a%X*6+L2u2qaZ}aJWc0xt18z*R+WvFR@GU^ zWaAHpX3eUage|or646-_KR1k?g=exEkjQM@gt|{_HefBm=-%p*Xp_-$v|kCNj^Nnvp za(~ogTxy@)a>yQyX@o*onpAQ%NIK|GS+>zah(#NyudA~Xn7NIC8@)XmoNpV14HPdJ z4Khi+!en&LpQi(RCl{Yr=Qa0!LMHWX^?3*S`^uX8lxBP8lSl~XlI!rWRlW4#UG+?IQlxV=>X=niZzNG;dhz5AuFEGrY1{)LBm^x9;Tu8ITuclOU zu|1Vm5=u2AN;Mj=JxTnP=4}rVJ+HmVT1YH)hZl$dxSNfM3b=c0NUy; z0q{^`>x&;*XwuftO<4jQH>C}{46D!3XB3k!wh3^tS%c9+j-~blH@~JGFO431UgVBx z@dRAlc?=7#>4zaAs47!V*fJ1;?W8-dC%hGokUXC^-NV(MqivZa2^O6}0r%l+(pas9NBZz4LU z?`J=8L8+Krn$Kk~t{+mE1V=NiAG2)x4<(ju|KZcIY_Z%e%NFaaG7#sOWt&`8vuq9( zGZWci+1xV%7+5%kWosu)2wazC1Fv5}maWrk%d!M@R4Hi8}%gp!^Wy84K!&&;xE zo-NDfNw=LPmMy-YW0nngorz`BKu*uHjcLFIAj&JkvSlCy%jOO*5CKSx&KB>^meGL@ zoE4+f5`f?)+D^=}rB24&RFHtS6NtcBb<*zsGjzh%Pcw;Pl5HnYfV1kP-TLb~G0V1Y z->+xcw8R~jt-J5fDa)3C+_G%m^y^tR4QC3=mX`=7o&DajY;X--mJMuvWm&e)Fj|%^ z5rE8Ri8nLLmY&@t0w9*n1YjD==G~rIHqSr9O2>v4Hoi||*?2s$Y%o%1&9hxTjc0oo z%ALftedyKYav1qfVdSqu73Zo0Yb@|tr*!(O%--)QW!SG4+uDaG%5c@M>q>z^IU)am zycI4#a1j^Mf>gi^1NWf0h}s#X7P0nHY_+w+5agdM>%YN z5Z(LwXbP${Ig;Jw2fBGHp3l(~4C5jsZB1)+F3JzgFk5Gf?hpU8SE809{%$-i@8_Jr z8SZd~WF#dS7o7=BgXVG9LJXB2OiKH1XR;UR!H9~0GxuEP8cFp_K;WOI)C6+}6c#ov zrr?sxM!Pngg3gu@ak!}d0GB=rj&m^hXv&kBW9i%tc`Nh*4QtFuhBbFJkA3YFI%mO& zvL6y%a?T2pXz&zPCP%X=XT^N;%NQ^z)s>+^yy-=#lbo}H4a6uTJkF^6_ivY|4Atu~;XZDsfz3iXX5pdMIibltoDNC(^7+vqW_t19FJ%mpf6P z@=nx9IUoOBeC7x8Z#fz_m$<=*8TBbugZwE66XN>|f@p&(m{3(P&slj&Y(?FsPxMCG zbo9gJU}Rl*p7}3;q)@i!vBN;wHs$h^&-PYj*kGV6h*m{->;V4&Mrv#q>2;MJTGJ7M zixJ~zWRYQ$xC!e`Uw;`*?h{)^fd5ZiMDexf`3QhRI!0-IHZPpGc}8QlC-3*L>bL#wV)5&4WVS zRPXYn5|X_uZA{rQWF%A0f@Y*6={I_*da!VBPw{{DuISaO1NVNVsbW*GvLLF0e4pxLM8z5xla&hi^ZuIkwnlEjN1I9=tI5%;x zOnDLg-^rtf@lAsrb;sJPujmb>PJxC77)b=rpc%(elHg!JQYu&l_Cs0wx_wgw^E)H> z5tWe4!Th%1s3ukl=4UYTkP#0ZH=t0KV^b5^;zSl9^fzTH5ImVPi~NjMR0qwaN)Fa* zHm79mPk6nFdBzI6CRsaZe7EjHX(1QvzO>K;?4R(yWA|`dGy~s~$kq<9uu9uMBl3zV z1A`Q2RIO|N&E#_aCd7E23D%~snyXm9neq}<~);kGZPK7Z34#R9+5~66kq^BNp(Jfg~(`h zQT%WENGc%LjANZon72sIFL9!VF`tp#%G|vAgXVRsv(4+kBQSt4e^mnkhRP)%gX^mg zHOC)2RqQI9Y*iUhn{Tp4?RSz(f3^Fa`s{zv{r<)FOSxbFSah6my#1@HCr_`%lwttv z!zm0b$!F_gs{JRs@npF^zzCnFEhED&B(!14n1{Ilt#$?{N@muM0-Z+nx8+p!H)y5o zHvYC(Q5BfsYgqfX~FUWF!?L!HTq39*Y3{34zK7PCAYwz$y3dIsd{(dyB{X^ zcp}rzq|R}uL+iGE)s1Lr-G`Fy$kU)b(#T0(?Ia66tw3+&-jvV?9Dp<$2Odl~@PMx1 zfN}%~2aa9=QQ{+O4{dM~_mlEM)3_nZ0L?_NO(%aVqc$Isa>+*$C56|>r?pi*=hJ+0 z&|=i`IUIv77n?_Ni7c>=|vM@(>0BCX-6*EtrC%>z_;t@bY_wcZyzn(QB@)2Mbfw07}W9 zVWp?m)_aez`_!83i(lS?uX^vt5VxMioq87Lb@l*8)^Gk4=ziDjDheLof9WS4Ja(Px zVuR!I+kxiE;RfPZ>@HRyD11Zalnk^Yl$=B9z(qCKgrFFNam(R0r6FPEnFjw~XhKrc zOJu{-OO~G`sOsHCjt|%Qx=Xd@aZmQtqf-Dp@?L#?gGdxw90sJ9Wt!oc?FTW7~Wqlj<6=G z0xn0QKr}A_0sAHs;GxCl>)-tGV||H4B`&Z!)ytyl$zBM;@~CjTxgGTP93RKsT&sUO ztcl+`q5RBb-=8x&U@jJX6&vISxyKJ$8_$pf1jXSwDyBEnVW2VxW0M8>OD4SQ{ z5fbe%)r?MFaU}G?@b7>=upV|39;_MyVC*h%dSCgp2F+lf;wqQSC)LQH`RwR0Son($ z87abte$d?i+QZF-a;ZQ(RwPG3!~q@tv)?POzD0Lc?fSmg9;OH8%`>#I&}^31;esL- zcyv3aSYE}p?D;i()N8{}%+Od2TsSU5z%HAy)XDOjs(5D%AX|fGlRVsn9pYNKvO#8W z&d&eDlzW@ZX`A3bk+%u=Pd{%!WWH|*H#6c8K4An*V(y)*twv&b}lX`IzpK0 zF&EKJJJ^s-_khUlHMM=a5X+DfU<}7duO-S5?RT2R)y=TxQT+Y`)aXJ8vxM6{P`#~Y9^2~n@n5-o9GE5y|+RWF60C42%f zZvR-x)CsXztZ~Iszk&-yMT>6$h1%>+vLX* zhlCUbo@bCFZuUecjABo40HT?x`GcZy_(=`|z8>iP__dUC(T?DFOa$Omk1wrDv=t4c zvhL|ucugsFx%36Hq85QRE!SQZ_2SaM`a%Mqe5hE#rO-ZcN*s!-bKs25NNGJBk|PIN z;EZdpqIY@%K8h%zd&Rs8&I{!-mmXs7YNiEqS#V)f%;~2$E$cAw(w=7gAg*b1$-s){ z?g!a8a!>(`BfuNqG>@GoBT~m%c7SxU8cs=36gA@&y6mR8h~r5ncj; z#oCWKZl$05{J6-3AzfNF2!QMhg%`~0qw~e<_8+#xb5`OxUxw*Ks>ORtounP33Hm^5 z@)dH1mLsRXHd`xg%0G|O==EgOf zQA%HcP^0BhvhQRxffel(;&ZZZ^!NcA8yW`9??QL5z%<`rbP$pQ;%vHRhK|uhRvb-2 zr1k&HMiYi1qlu>Vtu&gDadN%UfJ#38KHiLlnhA#CCxn3Hva zVqwy26M7K1@)uwE00W+eEojswJXp_bt_WT}nK)$n;K$a)VLzDJ2@}WQz~eS?44Pki zZfXxQam?B}!~XhqnK-1^_%@h0HiU_T%Rz5Fd01hC*~`zF&!A?<}83#u8do zsD`qBNY(=Az3n}tXv`)Kk*Y9pqzCvdn>Z9dbWb*M%tqB<$j6yY909AncTFUZ894G^aL7{9-DZn6Gcz|N zk9N%#B%Uv{boWa0AL`d^wvf_I)Zm!c_EvaOp6gg9W+`AxuPWte%Z^wMS0U!Z@b^l| zyL#i!9t<-)76=B0^lY|BikXze7)4B|;UbCgjQ9hBY{JVI)(SL4wwAJm^HJY)oQ};F z&oMq@KEj~zZ5zrMUGM*d))2L9Px=;$mPXd-b=AW z7$4z9*O@Ia$Z3J1%B)2>76`+H7wUnuP%M}Y6f78H?ieC~!f`AP!Rejo)$Hf2^*%P; z62UrTO9WOOE0=9ez;er|+2%%CtPJ~XWjjs#8}3_8ZW@>td3DAImdoh#=O8z?SA|pp2?Qs?!p3-8{wj))0YV;>qrJ3=@xczhjtq zwEO*3`^C)3@HjACcrOv?2VQ?11Fc6X^4DKj=#^TiT@)eWhZz#_0vabqCOv`kAcNW1 z?UYOb^xAd=YZ}FT2aC-;f1H)SPh`FL88yoHFs3ycWe_%NlwaRwE@ts2?H>%+>Y2QF zSZ+XxjjKdH0^75;d%&%W4{PlnoYBLFTGNKy-(?IJo*M)1PZ+_5PPPUpj_eH5$b#0g zGoo0||S>2n>__AQgXnRAe)@Q4w*(ics`}epm8Mv{@ByhJVu2 zJkMTlE{#^!@*{_NHpiHSG8V5j+-MlL@e{E(Mcyvh>!6DX z_Fe-W`suEZz^r;b4NCpX8HdayO|cQvFjTDZfEMWLPn6MnRQczn>uzoS#G; zjXRC!S1`4xXvBo!iVk2R)W|lh3Bmi-<=?#p(NQ-EgU!qSVpd8cPAg?xMjx8!*K0xM7q;3w;N$ts9d1DV|P4=ndW16Jbg&Q z8S3#YoXw&8AjoFMepVXjFaL#vknT{W1HwhmBEIxhiMx*Pxt#JierbnM4$@9zf4&MV_h3bXp z7gXu2#iF}=EM(Q(4aNKkl*fz}AFe5+2$zX6(V`$@RRgJG@dQ54;AC&K64T+51S70u zt4m-EyXK+THLYL-<24qHDzT{|aq0L1Ah)H~ED-_n=!d4WL?lx56vI9>!-rGM9jpqu z2(}Rr0WkZ1iL-3p4}_%NRx6ft9?}DlDT-x=Oay?bL#^>bzJ&HW^uR6{QwCx-REy2q zAq_!<y^UdVYp9F;+W{|*7E zlhe(PIxJH^7VvGY8`gCewq&^PMTAl!p`u%DE4C--)-tvn8!U*E_R8|C(VAhccB`}) z%P1PrOP0$>_K0qgC^VVnqzPzc9ApTll|X`5nhY{AiOD#LLkxHb9Yin*Ax>flN!*FS z^ZW08?tS;whdwa&q-SA4-H&(gx##S&_dfgkfGQ^Y!+MTO+F)sff=oAn%<%{j&y$pr z1+W`Gnf?k7NaP5MKbi_Np}n(v6Y_?Dtn2*_a}$)!et@5xC};&)rgh%J0;IDPLON9t zBxlo4lnliM`PawV;^z7Xs=z|}ZTR|lBMC7^ifc@LClmHWt1Hf_rtJ-j=FQk8W4LX4 zINH~g%YWI6lh7;;$JACDYBdf`DtxCurY3nrh@>n9sQ~cVIK2;RyyB*i+K}BA`uGoI z;t$=x@T^~5XXpy>Hg*S4{sY*pgGPiYmX$|p&~so;P=myw^KWt;)Sx=w$)B!XqXs=y zy+#fCLiPHI@}h(=Uu{wWC4a2cUY&niO} zuTP)vWWycF5vFPZ%E6X1nzjOD!ll_IG9EuGN*qF(ErRM+eM&sko-(XV3+ZxNZJ4qg z0<{WeBO6BgM@mg71)W6P)RZE01X#3@jxB6(dK1jyd?&cSR5@z-sy(C}6%m~*F;}wC z@R9<=oE5I79I=vAlp{K`f^u}aGz2^%JPjJyZ2Z{Dh5+eDkFg^CQd81Np%^hft&l8A ziy9`wLoKFGaGRr{y@^^+*t_zxfL7|v`ql+g>?0Iqsd(M>ipBlRVc!^c02o|A<@(cS4 zYS-g6Q@a?it6d&yy;SXb;UBFPAFVMQK3b?<((W=a`D}4zPYb1y#mHp;_DtLv^6X_x zm92BCT@_`k(~#i{?`XbM+0rJyt0`NG$Xu#yRd`ULY@tU;*+S>4C|ii~Qnp45T>^WR zLQ#ftp)lCG8bI7NYC{(G@HGXsu#4t@0R5qf<)1{Z8CE)m)1V4vi#=#oRkmLEwztcb zt(GfW&G}E^@li*g`%71T2KtfM8G=txw#;25;cQsh3IH;}xRFbKl_vGG-}(e+Rm~0U z52h!DY?}k-k>%ejl&vvW6;0V93{T2dFn@!L|2&kf4j6yNY--rST=W)8TQSY%yQ*xB zxw6HfqHJj?7${p~g%=7LQrbIRMLAhf+49y**+P$U2Fblul`TlCbV9+)q5ENF3uh+t zEK9 z(0F2O@aQ)-c(j?q6N4FM*b}p|kjmsDeo7}AI9sr84>?;v9~7=JDLw9%khT@XX{l9m zOb}p;V?sGpQk_hF=;X7wVP;u1JhKBzE;2i$bD%kV9`IBhD4(ugqdEL;^%~9L6V>bA zC@%`lVInwCW{=481`@cdr_pM)A}x6)$kRm=l-+_R9JLEucKE-TW0*Dx#ylkdc(D;x#0d1eaNFc?2sY+okk^|N|vj_&=!o^y3 z4iVCo3ICp)D@sSH_C|KhWxdg|C#6B?f55(c?$+AM=MdVV!YpePy9xOm#>kiDb6{%N zW-HGs`5dQ9vGb83v2zt=MzU5=W-#cRk4Al}4s+Jkj|Px?A?ioV)ek(mPIeY(NKE9g z`avMzu!H6$R6m+a)sIF{KM-ZqYx-TPAF^DyD#0R0{M$11qw(LL`q2Ovq<+XIU#K6g zmGfHu+2|jC`Re#m&t_3%VSPgcmM+ym;J4+!RkRVw5g%lNarjJ_4kEL2-J?astl7Sy z;~Gd8cMqYNLQ=BmnRo(S`jfI>nY$;WzBSmOpH+p3cDK zF)HmN39g<_>FVj^5A-Uo9(rM}9vji5yLvQ`>M#%~yI3EBH-muefvbl{r+X8?=kbX0 zrT_>kmR-AL%in1fjt#{GWD`(LOFr!0+Wa$}UX#e93FIA&mITwn4ph>dVhU9C)pOey zD7eNwY&cLEAuQ}Ck^UYgrrDUBqH9?vE#~wZNjoI8P&pYU)@y?V+F)CcZ8DLVuc)zJ z>XSH5AIE(X$2EzQT!r}~KsKe!YH7h55A!#gjcfk!1!?})=rF@HlFK?xY(|*Bi(fnT z%IspPh~}zmX#OtNRE=_A8rvM2n!k&)Y0V$iLG@c2^r$rdG>BE_+7>81ak1tP{4iHt zTFzgZ|4l`EEP<=c+VMtIi<3GG%7^B&sbN(I#cYlbW|OkLgW06aqeeHKeu))`lb}l? zBNW3#2i(LU$AhLw-j|PJt45gSPyJWdEGxsX!X4wKGAU^+)iC3w00m0w6e8^^>c=H{m}qNjLyO@u$99Gi4iRvKwQLn&;s&QxW5?;AVHb*<}J` z*Lx!NL`kaM}QTi9#f4|MMoYa*% zWDd4|!kwaAGPXi92RG;s1X2!1uMpzTY9d`l@AJy&o=}0@!aPWhBbFbdzd&e>8t5p! zT|5>*kPU+%nOO}4@`xi)w-ki2!Qn6)U!(SSm@o|EqE`;6iZ43BJhC3^=lsKF9)W}Y zp<6Ttywt3{usy5)0#7xDxPp1)Qb>bYfm7#fXe#T-JHP1IR&l`-yrf4u`J6NE#~)wz zIi?UETkl;4o2Soa9qIT)cgl(88An@$$hl%va;hz2Rjo7mfnS?zp2K%N^|Q=Lvq4kGn3@Eg%(k+E?x)*yU(Vfg{qX z(yw3)S1=q1QLzFmCj|$g9d+pbA?Zkls3X zSAT(3cwO<677>X-M-7CH@fbPCV|?CKQ+G(T#93^5w4K_mW4cvWRwKXPW`#;5r|hB8 z#rUe|?SjQRp&;Q7m|8HPtsC%F4+*}QC3!oUlPt*=x7tUGB54>7AXQ3CXT7g)R>U9G z&YXwX9EvC}v}U1nro27vDL__%3N+i*od33os}bk~F#%?m&PTn=_$tK|FXgKvSQ+|b z%4DwNf>ZcFZ_v*TO|DUCo!c$ArVUF*5lQI(MPkl8SnsXjDnVATp|mVY)@T9gm#2^#^l7qW!n*gI%ag$5Dr zn%W(S#u5Sh5^6kCqP-m;e2HMM3E)u?>qQy~y85L$E-;%No}aca&v2I77qz=V_@cHR zV6%d@zR1F7-{`t9e+m7|2!V)rk51up;P)`2Q%r1s3T)6~3COf$Ui4trP*m=b-esz* zPVp*`G4TZgV5=cH3c+bAgYE98*|aaUlhU~(5GV}`Bq->j=D8tH+i(5QSu@GH@X=6slw_Vs+!l#gj>YkGJ3YuFhK z*!`wMX(XG}$~+=LonQ(RD_|9@BO~eih{Y4AuuHl!Fp@v;0j=L#c`$)gWpN(StipJZ z<(2k73#cHt4h#nutF8UT0<2gKSP;B!)LC8|nNpmu-&|nVI-QEaSb{TK{5)iu0M5m88`V-!QUe}kJhy@zuUDo6_c#-Bz&Ksd0W`yAW-L z0yoVEyRA%Uw-xZDcuS9m-B#@RSkL=w4FbZ7-B#?k#DCKxjmmB-YiXjo+X~mH?E)6v zw}n*#*bs0`79A0>LCGPH$|OB%h!-T^ae+%zVv`{>bpcq5%;@e1ZC+1(-;pa4)};Bx zXCGcPse43w(t#lgpcP&`@T{c5SrA2NC6u|CGXgFM-=*P)1LJ81 zXZnSZqiNj47ODOe5Vp092{u{6+EKb7PnJ!Ue1(-_N{qhC@f&&RGIE7`m-$b=7aFq79urH1prRdF zO%cfcy(H&T^w&fiJ}SOimvndg6$x^nr$2^~@nSkz<9A6CCPUgBd&w>l>;MS(SnNAI z4n!RaA$r)B8t#}|g?>o@ZY5Cz(MoX57@X8`tfrOM z;GK=_eQh6W#W7|q000HhxKRNBm1DxMOzRsAGK{kOAYB=Rx}Q=fo6J6o*Lg)oOg&bQb6(SKh&dY~_r zpLdi!#zGrZel4M)u5#>bgc{53ev@Z8sVmOU>mW6_Q$+D0gBd2V?UbVT(B+X8#YR-0 zxhEHrdZqVyrQ{Rf7}9H@^3#*s%idEszj&^oElGxf%w(LgiCdR`4!!3jpDbvXPSx|P zc0;w>j$X~w*7G0I#q81lK^GY{g0>2|#9mSD=G#YqIQmyS1P`9(hAg{UBkL+&@{xd! zhL*>eJd0n_LkhhW(hd+*Oj5iejqgPoYKe^i7u+1H6@+_Qs z{g2$BU?_iaEQbuB^+otzHb9eega;DfCceC4*RXdRIZG4ve(5pwHP_gndaM=6N4Os zV;9F09&GgthW11n^Lb{qlx`2Ln<2AhB$nryu5CK}HhB!GLK*o9cxj02mOW0F4=A04rnbbzM1V zFr)G2D5Zs1^o3zvt0pP^rt%`L^E-x8cFTAg~cezJcX~EjqAAi!kvXi0gOR0??HIHS@JQ zvo+R&glpcP%BJRP_YSqM(kwD^i~yqkh#EF456cO?*#)RM1BhSw+lKNP>-4^*!S4LMQ0CN$)}>he4!c^Yn19 z68tg|-sd7^y=mPb@?lH#V^`JItE%kHJ(|)E)G!eOnw%-@03OC0>Yyq-=J-I^KUyP* z(-Z~q1xXzJF+N8L8R-<^iZEOeOL{AEC)3yte1DQUy>QnKD2SL|sF? zn}L`0=Z)6k6@a4qx1?XFbuEHRxRgj7k^3Y80iZPgz%I)hurue);R%IrZ8tGU(PH32 zR>YwDG+kAlrs#*5CLz-{N2aZgOmmJ*n;e;DAr{)%;Iwn&3Y|_rQM2xDj)+MsQgpHYvxt$c zwwW4}Kg9Qj6Z-khE5jN93b@4qZXB^bfO7$E&K2Ge2>Rym>*nZ<#dAVstS1PEdd8%_ zX;w(hf{Yh73#^nW0tL8izo#nUNAXHD0{<0a5Q?j zx{zgQIPe)Jb*{L(wYnq%|w&$wabT*#e zEvvbfK1(c4@=LLvR3DqZsx~8Uj}!Pn5~0^YL^I(|Tvh-3Ur<3@8aOEf;I?w@*i*BXyW!{%mFC1GM=w)oZZxtJQ0;^Gx;n$?}44HC#yF z=!j%v$IC3*unsBGP25RYpCzD)*10m8&M>}8-9s*w1(u9r2?-FTY`jc^2owU}Qk%Id zTFXpaEW*DufZgQsr$ZzosSkfN#eOhW)a*FgZw=XvdT#`S=#au`IL!7W?5oeW9A8xi91k)eTidz>nBWQ7+ebaw$HvYQ z04WrQaoN@WkwO3*mjF0^xX!whbQ zO2#a=vvZsZ07G@$r zYK3G^Kp_95oLGFux&aN+aFMx~t2-W=t;Datc(kUb?R%2Fg@g5?BigYxae0^6;W>H7Fz49z-Uk2+a{xXpnS*V^q_l0Al34Ep zC4n3^&_GGsoszD3X(-9Mv4oNa@J9DGTm|O;RE7CJoCD@T6JdmM{6h(2kbuG~t0WLU z0||`dZZQ%d0K?5L*oU>+!hE&b+Legi4Dy9;qFL6wK%ANvG>#W}UW~W+e7K>!=w@BMf|?taEy`Y8~)<>W#x7STMA)zg#mylUjcyokifjON2*IH ztmcVl#I{&X=1*{wIIZoFh*f^`7$Z;@-=$DGWiEhrE&pbjvBr}d<2~q9FyQ@VK{o3D14`E%n~d92Oi1VYYd$t0xo|QH3Sf9 z(=W56$ZsQqCY{GlBc@myZNw4XXfs&a#D-BnRp1cBolGcxsUB!b2H#9qWd#qMgkt4RWUQ@^1o8av<9N8Ui&R^fNM z!8VNP+f}TV=Y>QHtNNxiPWd?n&l4aEGKfG-*)ZMLFpxwkexN&{#6+ z+i)xUkf(hRDT6?WAw9~$MC8oSsBbSvz20A#;RV$-V$QAndM&^Ib5`_U{bc^&Jn|Jj z`K3>>lS9<2<)05{asGvHmgIjD&ImvVXZ8F~!&y6jDx9_QKMQA_{Hx(?B>!4C>*h~~ zv(fyo!`WE=OgJ0QzY)$R@^6N-H2?c>rtp?${amKHrb1NS#DNVhc5z@Ii(5Ic&BZMo z@UPy?0aM;i4qe%gLyLUns|5-CfM)!z(sDPg~0kj~e?xz`y+J8%j;gv|*j zG1F-C1V_0<2ipKqMow}fRED}hH@ojxS;q)rWvDS6ck(awr5NNg5z2E_pqJ*oe+TF& z`IOI#=i*Ed)@KCa@o9qZDxap7p?$t9%TZPz#`MrL=(iX1Wj@72hU6`3_?u#4_Jjhp z5fdrlZIrqA418np)W>m$Egt{aUDvPcnaYodUBJ-&vJCx3bpc{g^w5pYcvkxh^6NRd zRd{iysZWWWBV6m&E!U1o>(#0yw6M|J4@Xo8JV!8q{sx<3@CL%a?Tq1Je^QA=tF%zv z86#e@GlsaacE(7wGe#{#tvM=}jY1H+Y<7_t^W<8aVbl_qjj&6G?MI*Fuu`$r#Z$Hu z9M`|9t6$}6R1Y4ImYbh$E{r$gq+4$Ya>CP{8W}Lc(j0H+iNAf>!uUj^)9eiXYuO6* zp63(3+-kFX(c-;}rw&n?s*P1MoKltYKZet$+&0vqsh|j)97BiD#7eybAzb5*p#+VJ zAtK)lV<`8zttZhF4MSVI^P~F2Qs!6of!!5etPg+vJ&Q*VEc@^`-m`cdxVoe2#ToI+ zAO7Zh7EkEILc13U)GL2@@x4A`X5{C-w0Iix6=a|Pyq~Vzna;}dPJG>p^UnPx^WGs5 z1JH}VkV3hSX!S5=Hc$`;(pGHj=D=t0R+e1I?L{of_8D}_VJ1JIzEXP|Lbie~;dQl@8lZ>?k z!;S@CyQJ8lN_I88-1O^$Q~G57u&p$c@w!>ASpY&l-!A&ghS@6LI2Sg+lI*IT0uZ{A z{0tJb3S=zVDj@4uYKk=TSIR2L;=on`+FcJZY!frZVYbKUV4Z`SA}990yHu^BadTu|4-~c_)mfx1f zEbUFBRlj*Je`bZWw_;UPrzXZnqFPM)TT^VF7w#Uwj3(;xuMFK3XjNNm=q6D^bvZeP zZlbT&mHsev6GgSIEVZGVO3{>zJ#V7mbIis%EP5$KI@ zFu>7>ICQL$e{5MhzGJ`~9fQXqGE}k-tporBRwj{}Ck;>ZZQP_gR3Rxe-JRU=y^yJ* zRcN}4h?CqmV;x1LEX2u5vG(ifpVbj?in7PJeptJONA`uCDsPVjDF-XrNOWo+j46>` zTvWrnt%yvymVlC?F0Z1K+)0cmbg*x}!vneD zAQF>Z6*opM#ZWF0z_nZ=VfI@y%!6jtmH3UuFUwhxIdnVk{Wro&v`!VLVu_ohw@Q*q zx{265mj~e1ym!0Oa6k%+n^fyR>aF9M;`L3iaOnbw`v{mO8 z?Lt@pT&|Y?kpf$UzX}u>+%bucZ~v&eQcW7VmF0!bU`h@tL$~;io4Mf$L-^{v!??R? zRsg`{Apl_AP`~k;GXw0;^IU6hA5s4JQ`_acd2bgAdB8PD zrGV=r(m{|8!K3`RMO=eAoa59DCg1G~T)N4Z@4^p=q`=Zkh!C+f))DM3oDi18Jw2{CILfzkc?>##^qZD$k{Wr zS3qDA3Rj*Oge&7sIoz8FF1ota`82Z*LlDrII~)N587`Vp1t8N!)A4mODrCU!rWL0g z5Np19LOlF9jL7KUksbp?F_&_UIJNaV8j4ki3>8ATQ7>*-a)3pQ1{PL{J(hvM$&79U z;>sGt9>Yd&wyDrK>Qd;0)Ez$Q$YB|RmO#gSr*_O+gbdt<#mKD8a6v7uG{mrFTU-tWh#J|xrG zFx%-DN!~aKD=ZM!xa~T22njK|Nq&hgJVqDANuF7o77_G>#po(I1Zcg98=`O-IFjJO zv6<5ZdWf!*GeVos?qDtzqRZPGqkk*^kJ>@xH0Q#<7H7VV>1EO=Ygypjpd$OA#HBHW zx9aWL=wDxs4Nfg4wxZa*>lCtU_bgN;fjR0?0%5@+u{6c{f-`geP<92;$BsOTfKhC2 zW%*E06zhpEAnM~m@8bdYM6S0yY(SqChFPsQT+deAqGq5~iX1o*f@HOBtTX_;b4D0t zpiMcV92lENOPJ^}yix?p7+$`)zA*A;13Xwco>*GTcNDHmsMUTS;0%kWRx}phB5Jph zC%qytl2c`&>cE#)3gT_34^T3KOu$*=eSTK~h2`Cu0+s};3zoTTp`1HBX9jf=uZl*p zGLC?WRvZCKW}O4;*udTz_Tj($Aa18@V9qtj!vt?ek5C75QC~N5v1g##-f;g$Xeyt)N z*+U}PrfLE)t48{sCU(+}%qX;FC5i`4d_YY!XhQfjxc#4tmgQd>G;K=VvZm!9^*&`7 zZnZk`XEgm*JoGM|S)p49>9I64DV^zG#B>x1U9TgJ#0n>~fqqoosnJUfa?Z=-T5EGx zB-`xcK1fi_3GqOsYl&|U4lh_9Iu92@sLskNdSD>D7ny80kB;p09Y4ABs2qrAJNc3ec(%96gl zag((J+d!wJ(_X^5=VlG~1m=PKz68x$ziA}4cZV{>CAeC)rRWzTwPh~yhk22q)zviJ zlb0wAPY<1KbU@3#CZW}{PsHZfW0R!dus1^uiqb7IT=HYFrC7EZ5I~bS5zF~zf-?EO z^ciJ`z9<+(Ohivb`Oo}gfli1~Gol%O#fz5Ah-OrW9dy$8n%a(9y+^YqFO=QuAJ%8yS^dn}UcuI#{;v|krTX=2VkWmnCmWI1| ziPiRCLHHQbw^DY-Q?XJ_anPczp>ADsB&0P}wXH&;XYZz0s__g{~`_)E}{*^1T3< zCGP>4SVFb2o|82LObHYTl0=pjjA(WxMl@StMATDLoFuEiaa;Nn7!e?q;88SC&YU_& z1FCjpgQ9^dCo34y2AdA_Huv{#T!$apX!93t`!V`g?*3CRak40AFn#y$2d zi(C3_In&ECluKCbC~Q=M4)#X)MXv@Oix;vjI`IRMqhl@qj}UyhstTD^C0TfdVOY$x z9#*V=!I`v8Bu&;v{P3oP zN&PHtND1|f0`j(ys8p+lNJBeA%ws^xh3u10TYOM|7=huM{5wKno)^RzxDhX1{X}EH9dha(zXo~1OP+A$lpd{8G74+ zglvx;Sh_%sUfA1#YXAZ@uJnrnHl7aUE=Ff8aUzAbB$bZlxq9jUxLy-+)4JhZk zhDt4FU~H(~C+^XB94nVIL)O*`GgJVRy$!p9CEyDIH*K-F0C*rKiy;q4*NNLqR@eTy)PKIqm^o^Lh;8$ozcV(@9V-rjV8B_(`x)MWs*x}(OdKkI5M$i*R zgl_)wy}eNm|I_PuZZvQ1%UXBXlWd&nPbxeL>q8U2pq{_oLOoPChZ|}#u?I)l8JB_bp;*diyMa&s@w;y13Ij*RYXD#*|H8X}3o-F( zY#+oNZsR~`mdYKW6OE8PnLcf0jk11MDgu5M(lD7BV-3xPtl_dFwxJ47X7Yd&t@9Kq zJj5v%`%x>dv}vwNLJDN9seD5Fr_uh5?I)(G7|B1jO5f6<7gO~u(z`IV?^fS18&vu> z>V0d6z0QdE5bm`w!kG_L?kGVXB#F~6)|^>%K$eaH%GWJwB@DqxX*2{&u-svP#waCi zrIB@a=Uc=+sI72=?WAi~h7MX2ra0k0M`8%Hfi?zio73$((6;P0lU>d-MsDd6z*}wQ z$9{o0ov#Un=xiR*(+>h%zaw-2fKp?99C_s9Aa+gx5p)gvx?pT8_4<5T3Kz4g{r29= zOaYJU+*lb}cLW0z_Hx~Ai&~KB4Bep@e%Fskar8d3*G<{Orl75; zEj_ssVS)T@936~*yqQGxxLsrHguq1khrSS9H`dZTweoiZgq&S07zy+UisXshMK zy1^{vzf)XNbx{mMU=dlt{q%eRCz-ul1r1AT|Gz zO?^Bi*N-)6gIX-fS&n>NX=aePN+ukFe z?GC{W3h5O{lvFKrie3+&xl61|$kT6~?sr%weni;DS}6?du(;V1i9OLcO`rUU2H6sh z(=313RSf-Fd$o3U?GGuRib~86$$*zBoaAm&6 z{9y=D009E57DrYZ1v4SHeEFhyi@O0AnML^lsfHz??0vD(G1h#~STDl6Fzlix$Vr%H zQT{JvlZo=*c)J#0(JNfKC7a6Cie@nthpkR#dast#V45Pg z98&YCSr`7I25_tcchjV0na3iN80M`O5;}{0jyN+!0{|au!RM1SlKy2ZuEJ^NfeOIC zG6KX08TK;uu^h|4^0C~r+E~Cs# z?;l-dB39{q-#dS?cOF*blmFs%9%NnK`B!=8Ph4Qm)FaSx=P6 zmwv6WjQ*@ScC1ocbIaPA>3zQ%neMIAAyt_GMs<$1{_tMfx?+X4Ug>SIVW!%mxYW7Z z`ajjyt5#@h*4w(w+ma*koc;RBJ+$@1E3~!I+j^O|HEC^Gq_bPx5g%*5^M$!_OV)+5 z7ucW`@{vJ%zy?Lr@(tV07p_vS4EuWie)a3>CH-=?SQbn&wusL+qjPLetsebmn9NYB zU+Ynr%zrpSga2YlgOm#F{Qz%o$BV$*i?lc!Z!dXi@$Msf_g^l1_r~52>iHeLm+Nqi zs)T912~GLPb}nn|m>S!*tg%~ro7BoJJw`*$es7A#&V=6cP(q|zaFNsEi`5; zgG$72)`M^Ay^7Zf$Lb~D7b!Nq&XQM`lUch8pp?mcH;7Hg<&q+!3??zerV}}+*mRT! zRcoN0BkDzOOIBBG`dnq9NokK{oWY(fHC1&U=C|-b7RW6Pn&~Hip zapXj>Cf9mu!S{qVV4e!G=}8%zp5*VVY^ei4=*Gl&T(fY&`Bx~AJIR0k_ddZAU2RTr z)`<_zzHVUzKnu-A#q3rA2|||!1Qmp5QG`V^l~CzBRGy_{)nPD(B1nG#h@pWg1`;)*4i}VsL!9zp zLIVx_Ct=GcqJS<@w$VcRqU>b~*o>6zNcw9ws|e{RyG%tO*g{7|AfoJ*N{Eay_H=v` z2Qnddarl0PZARHTg}Acek22q)>=m9ccBz7((N-n)N7)AH2^3zJo`6a#Ge(qspY()C zCE2%fAW>tE1LNPs;bKpS?5GA{l=aNxp0Ty!dag)unBnkh#Y#jOl?IRrywVJg{C&L| zSCj^)P+OF}+zQ%8*$-Z>?J%j0cX`al@==!A79CNx$#zGJyeKm41@Ti@0{l#FBDfjB z$*M(08Tk5ut~a}Y$#Nd4_L{FVRKK_B4{^7$>tx*Xu(vhKetUVgCj1sNU+HsLW97ES z*=1A5n2Tzy-E2}fUzSnn-JGH3Hoh#Q=Db}_=-2fW4)n`$U9QWt-I6?~l$nUKOI1wV zF4_NzNsvNemu8Kb!Bpj!rg~j0ONaex#oR7JhW1O-T`311F5{eftV7i^|9W!O)NZ1l zqIkRhiK{6GkzoQ9OYLr0YPYHxa*xz*8Lmm~#{3*oyOFq++HIMVcD|>NgaV&cYPa3k z*T-tRyI-rMc0*tba1m0wA@b|g0=RvS)NbVh#e)}ip~sv%wYyQIcBAVd)svdQFgKzKwP5EUDcvO}$VV_3h=T zS4r(o&Ys#mTBdf7m8sq1Woq|CncAJ^MQZnCnc6*)7pdLdGPS#t7pdK?GPS#1rgoFD z6q2_aWokE>C*fXPrglrg9!l+I5mIV5GU6@{Xp2hih7~Kd8x>Ni-Ke@s?S@S&wHsEX z)Na_6QoG?nGXKCbmD&xHRBAVDQK{W9LZx=Y`jpxYvzvu$G~q61Pwgg9*)!!y?UwOR zN!@QofgeikE-MNQa;w2BPoP|u+HIMQ@?k7VoaH{43N_RrGE1B#fwGo=X^`5@9v-kl zs7Kte6;iupSY}~_Q$?Ym0Y)pOc0)H>3)P@3e?3%Zlzq$0QC4{s(-Zp%Lf|RBtfT|z zTu9#iuB3LW`3p$wHm1ao;6;P^=mGhdvm&xq`)h|sZHo63Hv@-viqbJ!* zsmM6s##YKkPaN(yc8Y^gyRU{=hMq(`dw%K*RJE1o2M4AAF)z`Q|Jp~yI8Ra&`olzI z*YbOQacU3dhlD0zUoTiiO>@66xrV_!TI9h{GE9;?mcV7hajRq0NcbOKxt>dBkeWt{>{ zizn{-@pWuT4l`vu5#yO}F!FgqKI`-E|LC!kzx<8AKlYxxe7s~Q*5^O{j(0rqz9+wW z`02=R62i>mM^<s}NeH^#IRqgrE*`PpeiXl>^zItQw{Ct4 zGyy>KF%xqzW3SQ-bA(~#&;Ic@YI~6Ucjx1~g$@1sz1SweT0_|J5-a-+pQ{tJ)D*%F ze$rT*{wo!+fK_}XqZOFF-(2J-=w}z&XiyM4il;}%O zOc$VLwt6U|nk`BNXtoX>&e}gAGC-PXF%`&L83x$-#ZP>03pjYP&vsFqz8P*Zw_6)O zp%w1#W6UgP^T+y=)C*;AH8z^%$vq1Xf=2)4>%YGB5UGXp&-}{s&rcizqqzA`vxnxV z;AfEPGZTmB-~D_Ne)#j~(0u3U{7)}_Z1I`9Pqz-w|9vg|vQ3n~`0eEI{Or9)bh`K) zKhN0R#l>f7rG4)a{m?!ty5vYijxYGN$()+mVeY|4t zDe8dXR1-Ru|2Co{L_1$Q^460e|9&vU@z-B;_xGqPrMKlHNrqPyZl7)?*bA+kJMxi(6< z-H9#rVE!0ApWi34o@Y}RV1|k*j;8mFRUZb@Ojj&Hill(OR!;6HNx>2z? zHZl5bjxZ*w3<_$V6p*sa97+md_CsY*CZT+mM>E8>R3wBnXPL*AmO(L-BY~T8Z?^N_ z!{$XHk^CMkT_}F@hr^0%n?IvkN@i z#HlV~&Q;k>9J$f={6b=Has*NQyAus%mHRbLm?p7;@lSmGKk+@<#Bj#v&>%v(lDYSacbN zw$-%ld5VsyZTFkzo6*VN|ISZD6mf(9(!wSz`W$AVLk@D}eYg6!YX7uR`fB~#LjV5p zXNLM`_Wju&n>DF_APuBdB>J_O)oN`UA=$uxV6|<=N)Yke#fG-*gem_4duPgjaFN%x zxmaanrh795ATcv)GugEmI90u8QfNCOQ!hJY$_$Kfm(9o+&i)g!M)0eCYqFM`_B2O5KumvqbMkq6VwC1e0bJ#H%$z2|e~lh%=0CSG-5b$t#5+e}Cub%v zkd#*?5_YyJiKOI8KjfV|^HZxZCC%x8*fUk-D8V6M)khVz3pdLZ;BqzE&W}FyurcA~O9125j+V}aMG|o8z>?b< zlLi*X)P`+vJr^FPSs1SDPUZ9P*d}OMUK3raLZjt1{z+9TEwAn4RX=ij{&!dn)Z_na zYSzb+MbsU|vf<{DKx#FCPw1YP zN}|-n{0Z(77QBsjk{s2;Do^B6C{%C4cWxÐCtwmo3f%RCs)2e1dJja7(EN=p~_6 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

$$HXky+qL~?Jhm(ST!i^G>2WmCx=;yaa*&Ve} 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≫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 z z32>J?antvCo?(~mu%q`}69Q*LCIn%(Ze|M(ZAA*$!1b$T=5lkNQJ2{mUAKHE5NZM@ znASEuKj2*Xc@xe)uqSv&Zpm>=w&2w48?;?`8vHuwr|=PLEM&=HytOi`W>Q8f$j!&Qr_)@ln2f zA(Qx}Qlvfd70ekMv)WKrwj@6-D$DL5_iAkv<(8coNd;@$W%`JnpTGD?caTKNXO#zg z=I0qZN@lh9Jgsbo7Vx&EJpPgrX7%mJy$>)1eo2IWvL3$(T8O_0coNxcGNZ}$eV<{m zcHH_r2DB!zAXA)c21Q7XC?oE%9+d?NeTUE?u_e7NHQvQ9~3PU zO$$nIIXKvpR)^M0Nx@e$!f83UcBIAi`+KfGkQR=_-Dx`pPGbt2Kcc}IK!hE@U>X(;k%3WE9cm*rTY!fz=4tHks$BoGS> z1r1q$m1?J=jb!+!ni8veUDBd(@XF#A;-cED-mmU?ug;sb5L=7vb|=`H_oCj>irUC8?EG0l?lbFckU0d!zHX{tB=T}-& zJWpD*U%_1+(Cy8xHeHHbZ?1{xRvH!{iE%aKPc-A# zOwtB@U1m6EjsTqVT8qSw`oTm;!5WxeJ((0wCOk=c z_Q+OUujlVkQnw~&6Lx`hnfBmFe%4k@gp<)`(L62R@y2Q!o7n0x#ym^dm^d}^w&pgB z5s9Y^dMP4?R6_Ww-?&sa5YlVVjC|#Il{ylcoyNh=@_fBydxMT^in6?TglWU9N0yh5 z(1r@;O6(GY1D0m_r=B&M>P{pYGv7dru?gEUrZ)+^Q{yPfT1snG1Y`}!;wNjz5MW$m zn+Xoi+-y9Lv&oQ7%itQ>C?vKMz+-z9 z$93j#wE68&coI(w$QaMZx)7<?Hr(%P&WpaTsmN&%R#bN;)(t|3UCNdI0j*EA&Lg`kDxXT6C%h zfVE^9imKa2(L@v-5x~qvc_%z;AdeWxdc#20=Axph9!KM8pu<4jTyas4UZC`TV(14yG3$mHSAuKIZmg_rBJne{U**S zJ4@m^Mi6SM zA0-e$*vU2;c;Kg=i1K>tWZf>|lxNLVlz;ip@zjG^VJpsmR?nn712klB6=%$Yc>;7> z!+-wFFa5lhu;=n4<=hgt#@UJoJEJfen}iY<5yff>4&$mSh{pAGMl=pC>Wpv9uK?w^ zM|fmoXY$fy9T0^mfMBBoG21W4_<=d$o%Qa6=ja-~gY)asWZ@?$gU-V!=B41)eOBN z5lkmc2%8wtphbS!dgFP7Fpez^S&38|vJ#saGHSLEE@V_%$S77J^a$blY#|(ER3U`v zy$fL&1le1WQ9&e|jH);3vi&zUpjG2$wrOTPql$QuO@zgr%C={2n+J))#W$DeQ zq$9ZzGL`|#drux@l^DX&qPWF zVH4*UqXC6z+``NFVdt^-I5u53`T-9z;gp>d0e;*`-E#+X&Se-#^7rB(sF4_nX*`Lj zGBu(ndkuqGN`$EmJgEm=Z^pBE()ETtBZB}L1s_e`n4`V35m%L~kdq8<=9b)=A$ZC{ zC!8($Ov?_LV(4Us-ME{bNnn-9rdUptO@bTS*EX9hW2$VKI+CBqQ03=sx-Y5LN!pP{ zD&aB&A0{=YBXb(1XBzC+ACR zO+{})k1sE~hesTLVR1W%wQgtW7KCbDpiRA*^sa2zll3~tq>9uXbPk-Mc@tz84{IM_ ze2{3E901s{R&koqY`o-pG&LA_$E5c79?wRl(NVgRu1LT$gM(GpF2v`AgF&0NE|z73 zLNuctZl}w03ZK~mI0zpZiCu+=4Le&Q&JsQgP>8{3EfiuoD>;<_E^JpbJ|#3I}P*0WDSuE!irq@)S3I0#HlxPSwVJFrum~$9&W%>aicilfi(jnvWH>jA5M=wv?G_BXvo>01{ys z7b5q^;1ubwLp~Azrd(}Z$xsd=8)IGLLBRCe4HZblR*Z+%MH)HdNIO#4@?#$;N*>dt8yDYZwPLSvm+;iSt|B zvc*8spx%k3D*VjAZgP;n$lumyOy+`J`vtp^H@y>whkWjd%=};c6Kp*xDX9c=XTrJ| z^ySt)?&2h7A{C>Bzy)8J!Pi44nR|ttm|Gl57!^;5B%M|YylNFR9#ys!IyB7;D zjA=rH`3et4tbqBXCsXA_G1pAdYS_2Z!*r=-k!Q7(gp1%SI4m)??IY!lG-p)Olb?za zhuCZ(!!}_Kgl}KYOtN;~M0c2FT-1T0Yy5axE=l>jgA2br)x-_#mj58-B{}fmU65|MW(kZot_CvGIR8^Gv9G#v@D0sq*58lGUfOz7#HFuH%i&ZTh~S( zo%s%%O7m_0i58P!a3h?RY-;_6<{jqc-Kh`*h@vl+(eWS-CH9XZ6G0Sb)+%RNLWeAo z$_=0HZi41XLyX8>+I>QMT**RW=vXUs$Q-x92aUJU_re8D#tADf*05D~R0jg=pe*K& zBG$_&;^k!&@#I`lL@ugzX>k68C?Xd%*0>M{gv-5&9UtO{I_?b~F19aUoU1QfoZS~L zcKae!!_xXG`l93BzU+#NMHbG~AjzEb{Fx9B&_L_YxeCUOQp`7PY_?a-#=i1A_UJPw2FBR1rJPSi@)>z{D?B54cV z`0ANtoRWF5WfD3`Q-uNKfvgiT=#28`V_AoutYGnjEEyDA{dgSFLxuuz>HK`gV_C=` ze-mX7_dOn67)6&wAQ6X9{ukAAgwf}!=cMR;wtD_l_XNdaId4S~#PfwY+LRQDa?X0g z_}oI&5X5AJo^Jt(UuY)DJ&i!wam_%PHBBdvFvo<%96hR$>(a!i-yMrK#JFcNRS{s| z9cK>GVJOyAh>j_V#BPIzG&&jFG*2W16>;^p4_rNS@Aqxrv2)MCnQia)036wdsRsU@ zM8y?Hc#Ki1SE`S8;>y{#+x)q5;R8yYMMwX?50V9t-^P}|QNG4X!ze*1VoFU%B(OaD z#H%+V@+{N-TFUQjG0x5>dY+4~j=xuyFAYrThM9BuN~D%?gj;uDXJvNITPH;SpV=Fa?^ zHs3!n%b=@hvGicspOq{s+ZtWpF~UAbpPeM)MH3R&EbiG1o#L>bi#+c2JRAi%%@ zA5mXLXG6ou*BTl+wbpxsI)hvcgn+2SQL)kVnIVM9!x@iM@x@R^s`w%du8xn?UcMj_ zQ;ar+hBrv`UoNkp&Mt?*W13;fjJ{k0X`S6d6|Kw0Y;>vw-57(wCHC6r8h`T|8|~A{ zRmJmHWn&tw(`r%1qhbTr>^1X_Q;qXl>+eynr=~}BkcvU8Wo)42M`MpW&Q!yYB$(y9@dt8fuocr-$_hQb4{Ydr64cOXe z=_4GILa#&wLI%WQzx%G7PbyQuz)cV)e2A}aY<A^) zA0m+@d>9oL?(Ec^qZUMx@jGElfj&HEw@$`<_^L(ri>2W-JEVU7#GL(lag=_2&8V^n zgnfLN9s5yr%($haP{GXmz>-u=o(oZ+%GyBtPS7pf- z43}o}$Kyzk@c5sniSd$a-nG#Q5(hLeBIZX@%=#yhaJ-VlN^q_9GjcjzWT{|cb`K0hqw z?Zk6un73c2l~@b{a%U2^6PP(^;4>olF4%$wb+D}@3n9W+`q6-`?(=HFp5tb3R9+O` z%#60xe>5n=9NUPF%%7Pmv0kh$rI$ZN%IojpKYx$e#L14H|gQ<|D>`N=Zx87-u4_EpnaP{&Jn$ zk)%indzPtV&w93_+zQqzLi;Ggh$l?NG1smzcJoI>Gvn({m=#OPLfd4$n1P5HK6R8z zIV};N+@V`nMB4#8RQ9-2R8$N)Sx7w}YD7MD&=}b1r|C7z4@0G%U8nBSl2lJ%Ba)ul zPN7$!F2VwOwIuZ^qP(}rN$6FmJh6aYElIT;R{Qsqk_f#DHGLM)t0lr!Z?TR=<7$cc zs<#} z&kVbI7~1B!S(>>;Duzv$7SN-i7chmN=IT)>O}YSyuq4%q=+)6#cxq5Y*6_4|UM)$% zN3`->y$Yoq7tpIElIgv*as!tKnf|?nt?u(67V#)Srm)gwbTc=LH{rqDC@(-NUyIqM z<{1d4H-@`8LilCAN0ty%6I=Ca?XW2djz2DKVumUs7+aO~7xI%>WN@(g8@33sWgMp7 zF5-QQEEOg%4XSHMQ@Q>%FEX#Gy#JcNaHH@ecXw{kyv-0nFw}VF74AuQSCUyh*)6%l zbcKJKtjwrbN0P(}3r#17(qhi>B(wYiu<7JTd0mBMyVsAF*N^pFKT%$PzUTT2Tn`Io zUMz0dYMHN)clZk9sJmOpEF8VUJ?ZYgGFMMXrZvZbx$#&S4c{r=t7wUn(Z)+{6LCZ; ze=zUV3_ot|2|2sWhFQzXNa7v%H#pMG1=HGXxxu96TX=`KuuX{h8bJYlhG8~!XV>TG zQ(T)pYaN?)gaJW_&nKfWz0Hct*`(F&q4t@E5D#YlL(vWk2~H`zO`d{=6oc(yfBK%w z$W9~7Op=+691hml0iz!DXz&Y5%@6ssJ159u(^q=Ky0m9mmo}ylem=qUT)h$4ou8@b>(Cg}SJJ4948!nS28gZf{et75TF1m4=pF= z4CZlxinxPXEB)vLaep}qf;NQtPrDMu-ak25sYF3fU8o`nEdIx@JL1AxTBGT4#bRNxZ_r~qm$gz zVfiWdI3Bs@ZX(&)2k-8r%AEx1S^buW5w|03CYjZ4krJ>cx8Hp?%e0$6+zax$D#R9P z2n_5}W;wR4<3?LOCQ0m87XvsbhoYX9=bWrDpsljtf-_FGmWfpgOGK1y60%%Cz(3~v zmHtq=6+Ek~YFjuV54ki1PXnk48~R*GcZE08B7q9cGliYbg(7*1&_Iv!d$si5-7ZVk z6VnEGmKO>@;x1DDL>nx7U;4a7AcY??`|}e!p5Zph{Z}WKk(Q^NdvcV_J~?s~$w-&m zrAD|kqf6`T(mLGe8BZg1YPPr@BviS7eU-we5~^LsFI10n02Nwb`>Phk_5U`|LPT5% zm-Ex=jNGJ&ks>6cNE%kjNw#zvTBXN=nS9KGc|fCd07sK5jv?k)YQXxs{0pp7k&=ct z$esh@52Wg0dHTai^WOpfd6v=#=fUNSDAnbG$>#iUlF zvTHOfqNpC#8X!(L)HG<7e&#k^^YN5@v`Pf40W&hGAEaYT0pwEt|9LqoXw5ja+E`j! zPwkyV&RejmEq)m20vG^2+e$-3xs=p6808ZvHO3n;%GfBv+vqO=DSVXgc_-Vu$2fDE%Cw4Aj2uxg;fB0>&}alACL z%mZ$`Nn*YjvxK&r`cr9a~+I`hMSG_67^4G;#rr? zO&M3odjDraU33pUtVJ677-_5>Wht%(mu+Or>?ZL6L!t_%j|!^EG}0VV=7^qS%rICa zQ5hvVE2d>OHS%gx+vBMCw0?o9vPd-|F`%+YRk9@SS)-~e)g`L-M0JHK-msn(s(L_; zb{D7$kP6175^S%`?iNB5)WwN#Kl;&CRmLK(N)Su0t z^Ftmal_EdpXS`{L8naBAxL7pq+Fd2A50gXA=6eKQ??5)XtdR8YgkU)gOQR|ZKD7DP zrqZxQbclh?N=HbTbb_lvmg>>syRbwD)taM7h`7;H5jZkiJroJiDYmZ3XF4lh(`3z% zy!dZTyn^4FdIi7LIPGt3E=7?hA}T%!c3A@%*@mDj7T3%JX>nmRaTy|v;|rtsvM3hc zxHt5|kh*LlFw72FQ)0=pA}de&)H+N?7E858e)^aUg4So1Kxg{RrjD_}3iJJGJy~yq zzLIJt6=VLY0Hm+d_jMv*fDz>5|6WA(`xt&QSZpS_u{3^a;(ky9?;A)cS<{xyK(P&B@C7E#^}IdnTXySBz

d9HLKC_ZfY63!`o>D-BJZih&lSi%mc{VMO4p1XT0aRN#fid@#w(D&M^aaj9k2|GM za`aOB9*$x^~AiRYOQGW6Zk8?|7Pji{(zjnyn&QI3c_M|); zRwgie3LB1U!URI}CYx&hxE7{?AuUXs;Ai`fKQXiOtzqk9uqSWR3yhvK`GQ6g)mSk;+DXA@Yoo2*QmL$o z#1)$3ugTddLDE3>ykNCxjdUS0#eMV{Q|zv!6Auf+y4T#)gDc~E_55L9)~s)b>9V95 z<134~=UOD;5-Ru**J-g6F%Zb2eXm{Gl+WyZblY{S)H6pQQ|o^oYcCVd4^pv-!DP06 zBExnQxw{YJhq ziicFNzfh17Ic@(l(OhjI%;RVnE33cgUeD#Uldi~mz+@&v@Nmi#SxoGBQL zVZ3DycG)p#V0g-AETJ--J|R5oooZFQpeKwDrYG`HD7$$(1PSd+8V8{R+tNc{6(BVg zE;BC;mrwnHB6su-an=dop&ZLRyw&<#0*!d$a6tEIvDSB$thG>4h74O90kg|62{xFc zJOAP4Sntf|Ul}=E3a^c(`rKc^!C5f7&DONR5u`9;`IvCKAt`VRhlrkHKd9_$A4C)P zg485XWQ9~GbgG}i3z#07&c)&yoE9Y(frz>WZbREoY zw~YQzZyMXEG*0kz$vD9lk!r&h$)6{RVz!1<+iEjDdR)HKc4-;`HO12epxhq$zi3;7 zoy9VFQQ5PwK6@2e)UuvQ=*0n&qIf6peSFmT8!5KEK`9(mC_7O#yGU8+n4M*Q%woe} zaQq*PRUbr}tkZuLZ(%MX=ck4w!2$3w0cCZ1(9c>OdMa*5wvkKm-Ye8k*-{z?vMsEGbc4K!lVHi* zg`Z@)?^85%FZHq8oJQkUwlfaZMBFpzY_ynXN)XKs!2*ju{?|^s% zv*qGw84EWXRE^3TcWt>cR$WUjPIM?YvX#}Sc9C=tT^uL6z(xM($#K8fU~McH`J;t~FQCljw*CtxE=0AW3GiS_z?DB7^9o#xvpg1W7Z(vZtKWw{!6Q{7N?R4AR*R1c5Y z3BAx8HQ{_47it{a({(L5)h(^up)JwJrp|;C=^!sMe7J2(rCDgmBfPCgFmG6H*BQ&8 zQ#Q7tGe#TO`4nQ}X*I@edy@XhieUwBw|!QKsAah;DL=BI)6mQMBnS4gf*zsIdU>?> z%fvP|br3`R8{N+qh2oYIIkg1E2I?P!_MM%{RR9IPzKu-|JE~|)k*ihw z;f#U5p#!yQ71FQI@8RY$!s@HUJt`7c(@IBZALqZvK1$B+)cxw7y`B1h3JQp4(tMQQ z803%A^?0{9?0=P7f1tYZ`Xn`PlKd!bOQ$5RQxexTXfl`}-da@jxCH4l&9#KQ5owq1 zsFt@%Vh5t+qZH4y@A9foQp=uWAT=l0%B{awBdUki+j%yI4BhYVu*yyit;4FxLx=Gs zfa7^OJmMWLkTUDrN(aL~Fmv#f_|Z76ola}gVRPY|scDxJCSfI*O*QwxkXZ|}5tU8! zb#|VXW&DZ7zNh)6mez=aw!P8m2=ADs;B_vf(uiz+5DIC`XDzoy{W5|d35!gN#jNM{~)_O zm{$kCkB3Z{`Af&iEaE}@N?{`<<{dI%%+*L++O!U1q}+%`4yzttjI~mA@he9OBha=X zL1>@C#p6qWo6Wyxoo<-J;cdthB^@%AE@k<^++^O-Jrey3`bS+1Z> zi_BM%FnhsHP^8GyL6mL+kQV$=wl9u4PC1gXqK|ROQv^I3D^mc^1=A+k4iRR^tQ~Ol z0CQJdRS<<|&9b!#Gjd08x7Qct#iI>p4b>tvzT&Y|~Q zW=k!#A`-z2>OJl_>pk?U^qzOsd+axhCHvRAy~i%;-@iw#WALf>*utE&_^9X=Ac*FK z09F8krY!Qk&psZtzNXzw`ga1FbwLz^jdHl0R0qOnyV)A0wU>8qb*}(x*%#(TlHaHu zXPD!11QrPod1Sm|4xZpW`e(+bd^qe6%{$%wp`RS>?hk$7SGxN{$LtSX+)K8y4CBx{ zAs{d_%o=x5tMn$diVRS>H6coWSPG#05FWzWmCJ`rZFF5{a$o^Cjv=PWd{)@R6;{3So)|h@u+*q?Wmp#RO!$yj25TJ;N z<~h`iY{>Tv{Klc`V#K)es2{&&+9KO3BGRlu5vc5^aN7FIIAJ{SYF{9jaTSFl5pk(grT<)o&=YIApB3 zFS_I%MjdUgh9s;@u?>!Dg!F9}s8pH-*P=LE-pL~8&0;{}K1k>KaNnmN6G7+&pO2dF z9DuV$$^;Dzt--Kr5Z1j0r=fr9*i{T6x_Lv{zx7<8u$T>xvV$j&KVI$NkXqee#xD)* zg|>R*;JzOFtDe5zQp%bhsYvb4vlEjLh+H`U%#>+cDI_@ChN!aweN*m+kR_Hkd9xO@ z@^;kvMmQ-p@Z-ykf6NaY$*Q<8X?2~7rcft6S>ACpb|%nC@K91xIR zXd6dAJbzI8D{+}!AKapX0jf{992CtYW%J#C`spagr34ZkLzue|QybJGKD0qC54xO% zgg>LFQPtoffAQF-BX+9IKdNhF>q>6!;8?rYV#cX<7fq=;7hC$o$$DI4>|Vth-Sf>4Y~YlRpyspS~6n2g;O2dEeN(=dC(g5BnGZ+-trJEm8bq+^&9wya+gfTz{e zm2u=mIOC>FefIY1ThTzy;K*|A+?!qpjh0`}#!0f)jq5SRAh^a!VVzlOMpkcL15Exl zV`Rcc2-^MwWp}sr)g+>5ul5+};Ws9vK zp=m798+u9#rp;^Tgl0!yN49irMSz|Io^LIMvt$5YKtvjnBqrQ~al^wRR{deAF#6p( zg{P|qZEv3?|$5y-AfM_^spr|yo&70KI7f4HYl`Xx{ zd@6|;wtPfuE?}D*#RBY?6~o45yoDz%vf+w^_O!_M#P@p&RpKdBavGB^<$lr&3O{n? zJi``CLKc;QoSLHW@0cVldTOMyrMR-u@3%EEk)T`Hmj(@0dvO?_BL70G;X51YndkW+=2+A(LgX zsp7ialbMxTwvyD82Xg;rG-+&0@5%SDE2cy!U4%LoBc)mL07Nkxh~|qMalMRpK!h2| z4;^>CJmPv;iGBHDu7L)HZPcnBrIma)$1KL}j8W${-{&Xo2I{bJ^|o&(7NtuZ$9EGc z3%5JtStA?&_D-})lbvzGC8~hOWLA%kaZMYc1oujVI9}9_9QB|hSx?W^A6r~(+qU6` zVb6~|@^nAXIjmRv5PwkTX?rwuldO>v1f>Cd`P@7RM zXYep@`wEdcZL$R!S&_zVWd*e(wPFi8*qRLjgLh)OV!$>0u%IWS4K_g3I+rk61xH{h zg|j+FeYcf5Hd)XR%LrpgCZTGPENar56Kq*^%8 z!jK>{%2-Tj65BehV>YN~Pj4v8q>{}&H2#VF`1qHmlaEevNq;;=-rliY(Sd&@sN;v- zSMZ{V{~AoLeu-mVX@dt|!q_q=0ul?_H}$PJv)PPRJn=>RO+ksnXU1BL9P|3&h~o2e zBU7EcC_nLES($RNAu)eW$GeXft5jkJnPxdbUduIQ&b`=Uo*EqxyJ6QxAcDj@pMyU0 zn8CZgFxw1%fNLgoBGx~sxk5cuKW7tM)#bR-bdu5@z<{MaB#_9IY4=U@fQSITI+Gj- znvR8Uok1trj~y=s@ak9iG^60R}^P?iKp|5 zN29{@uc|0C^vBQBsMn54_KCh$6NqV2Z14VB~Xo_fG0F7 zZKh6PE*Qc4!5+X~&KM6}*#t5382(b*f&p zX2R{-BI`wiCa)KLCv|~SuMyp;(+kG0l|g2FSxzPVu5;u3npKX5-J=c>Zbt5?C6jT1 z+Sh);#)Q2Xm&K&1B0=C?lMQ5TBVT!EzG|b&YO^Anvf9Y2$Y9TsO@d|4EW(5)Olud# z5Y~mt8NV&uj?RHwtB!8CRW0r=Zjn}}!tIRVRz=jo2DnX63%Bvs{A9eD#cf3ibRnp( z>^a*+@__J327vt-oT91u*XpgmNA`6w$r``1K)Lo`XLpv3`Prc~xnKl`d`{h792&2N z$@7D-ak5_jGznr`^_Zn=h$g(cNGe|;ijX!YuCTc?N^_1TZd_y&Z&NQW?{Sh`9xP&s z{Y#~4u86+HW#u|X+@4+^!xa!rSX9y|4BlnVaG}ebY#{SD!98KdL-VKOVpbvjp|PGy z?YBICSBIG$airqgb1g>3bimH=j=9k?K^;t#LUjrJD3pr$;W*A)hR0l8KIC9FyE3K_nqF ziZ{Wm4%>p5r@C|5Jf?#q`49U#OFgJa3RcPCa@nWir!d1vzSGxzR$3zjp!XvQ`{n!; z>r12VBvnVYz7&Lo^`#t1Eo$-(cOQo>2|vf#OiggHr2A6FIDwW|f|a^g9< z{|kepJ4$cq4l&xdnjw6kjoIqWb+u6#rk6+HFoB_MZ$$Wuf#SX-9<`n2%kAOeRPg|; z15)X4e2|eFHtH1Znfu<5tr-&qn>PyXwdzr|^Uz{KOktYJ*?B}TU3G{;vulb(e zy{mYB2S=6Xe>pBaAevLdJFK9V^n}DNJsAS>5yGu770QTtiWaoVkzkCT*s^uTGV)1w z6z%00l!|dHqAyryNv>JGLz}SZ+p|1}&5i}ctwDNWmg!qS3g2L)ZTar}-1o{Dze^ms znF%TBnpu3;My@m;8;{4r2yCv2;CD)RjQZ>~Ss!NJSo9N_s{Y6>H}?#-smbA})9_H1 z@^YCyhxx^5#%j$epGB?Y$EJXk3eXhL&Q|Ll^EmrOD;lNcVwDlmNT2J;~2B1{pt>p!tC1WP~3(0K*5URxi_913yzZ zD$d_b(G8h@mVlu3EgH;XMp8i&M)`#-v`uvdqnwo6{Egi!QyV+WxpHp#mhJdupD?cD@qnjs&BI^{J;6<_HjDEsnDv%D`5o;JimmB86+Ok- zvhwV+oGmY(oL2dw()kyI0<-=3oK2+Ib~;{tWqi4McWHDbKep*)^fr!CIg8VbqneJu zevG;>>4(s$!}8jhs8i!xU>ECCSYm?9icNSV#p(u`GD;M$@P9H`TR2jThD9E_?FGUO z+n>j#0i3KZp1PL3Ld>hm#Y{W+*b}nI)h8K7>lE-}i@V|+kP|3go@YAGy5|&#QL;sO zU#kb`WqFRjObW)&HM(B0kAW@B!MOEPsQ#KcEwKJI|5h*RR|7A*-Bwn=*2~qeb*+9? z?F3)_8W<`;|A@@|I%Bi3Uh7Bj9$AATdGapEOPD{Zx-IX}?7`{H*j&T?U4JOUSFFI+ z-Vu(0X%4iFv^b)*)69X}m{}mQZf_kM!6U}etu{9#mpjl50&1>mw7)h>N~$6b{R!lf znc~%FBnUNE4A10olaXUHGo`|4jKTrV6YNl|>E5Kk(%@uoA;TOhS9`(p9zXKEM5eX_ zZVC~Gd%|oF`5d&Jr#6+ykmPDoSp{k_EFBtUc_A&Q&DIW8bZGqmzLcy9bo#^7E@^)& z@u+v#rUEA|hBhAnk2>~nKeDc3d1;`e{HG`8Px`@Q>ghi;mJUey!TO#*;m3OczFlM3r8QFl?rtDGa*Z8p&x7hV^kIM$S)-e zdP7UeWtU_S>Nl;bn6UeD^#0=b%P&W_qAs}oPtz8TH7$?K6I5~&xz+p@UJWve$=U7I z#d5cl)xac&#cOhCwO)>!&2{=9ih+QKIi92mjr$byfGPNx_$SH_|AZ~oZN9|xcw2lA zN~m-z#aG5MYx~7_rr8rOICR0k8HfhwcuO*FqblN~rk0$8)F&ud!vRU;Fg`9C;u}H6VfKW0J zD#p%_fgRpXZj^~&?7#C+>n(Swr)UbUm+nwgmBNOxS)=y$sOeYV;?MY`J`po%02@85X z>3AK*_oh97_1>5CUMsw}7eBt<(>r^@I|^q_=I?LZ$f5`2{;2Kh>hFH=vz2!r(EOSA zb{Fr3b!;^)R_kp+oA!DOAFj5rOU1Zofz4^Hw@GNh_bsgTw5LR`o?fx{cWCqHs%^6M zZ9{Fcjfu6%M!dEc;)wNX&=(s0UzjPs@T=7?Y>(SeVmg@#Uuc9cH0%ot|DMp-1JR2p zG{@4X^t}~;E>&Dt!C1)o*W=-8!AT$G<%mARRMEhtrd^ zzbEMT(32DDNx|WFvJQxz+~GY*Lr+rc$x`%dyuV+^%6|Q=`n5bz=UsT2>DTSvukp~Y zaqHL8_p;vKOBqGZL-#&MFV70SysbT>+`D%!Loh`l=S9U-zx~Rfb|>N%LTy(hhSNNV3|Si^#srIet6bz z3g8?PIAZ~vYuZeTr<3&#&hh}xas%h<7dUEk^s?LK1+9Kxz%>JKSG89NxXT^5RsgPL zz`a&LUC|FJQ&VTl=LG6#0QHjgSps#PgSsMsy23zxZ32E)Kj6&49q`Wy@HYm)FKWL| zfKNN%X9d8|GQhtP(D1r`G-L%Dz9clP3}{%>#`}dg!_n}%fQHu@8ou$6qDlrm>IVOd z^TvID1X9ipNLk%}y^u2HNLd+>veJ<9jgF$%_oE1xl%wclLQye%R<_?D6tx^huMa4C zy`kuTEQGzGA7ML_0%N}}j7_xu*rrCik~aQkCOAoFb)Luv_J)A9HyGCbN5Q-f(Gh8$YjCu%(%;yfnFmpVwWQ+|18emnJvx zv*OZZ13#@xlWX`{ekop?X0}YRr=|j1m-9mi>Jokk*{tKoB38|et!}0{!=1K~i6Z0F|{_u z4;^jsb1uHqY^n1l$Wby&l- zvSE@YLq0dv8R9OmqxmDO6&uGxN9lT{183&TF2Q%AJMa-t5wDViFeFR`9CaVkw!2|_ zBGO*jj6iU*LqYk$dm@)OTw^1 zeQCb4c!F*HXoQ>XcCJh`U?(}(C{$gp!Ak^*FO0@6qYheYDqtBS0Dd4PV1``j@hX{+Z0{$ z^P;w@@mNw~!8G0*Ku#ZaX|~U^j$|3HJ9EK<%kte%JQjEmlI$!_T8m(^^PI`(5lj}iYlCywHBR*P z@X#DQwaj_SsF*+ZgpCCQj4MaNDUC-o0wara)?7yL#n|))eqd`t_8G`r#}7~H`^+7M zn}WZR=ESltgK5a8gj>3!885KJmZ;kQFN-Dp%5fi_EUqT`!L-bQvwVR3JiGWE`U=kzk^10=%D0`ZhS86mKe*9q6`p=}5 zE5vTgq!m212qk*!JS)7+fQQEq@s-1bWpsQn)gt33iJ&X%nV!rUolEj3*{1wQar*}m zHw)B9`aykN71ZArsILp4qV+1Ea<(u~3pn>aHZPnCJ{eSxRv>(2ND#77dy&xS#5}tS z;d6rU>;NIboC<`TEe^s%zdk>NTIw&l9jHLKe@GDS>xXc46~ZqH!qovnY=;VjoGlW< z-<%ghcy)D!S0Adtxo=2t?&*hfRTa(;?FG(N0Zy#93Y?rR4$i~BH7}g11Z0%lTY+%T zkRaUM4`I(}+$RW!G#XzxI6s8NXnddo;qFC3nB+VAVR?O(TE8J!ULUB1bzPwrXN#lO z1HU#uEQmg%)~*UHI~NJd+_H1-t4Y42AMPG5{e?Ye&ZQOQ@es;%9_3Lo>ireacMJve zB%kRAvWK{z7LY?C?n46@wIJ^H3WPI5g78IZ8iqW0R~tP;@lOQdkcQ%c0qk20#h0+~ z3)TN(G91-^vLC`XR0rr61>qaQ0A&_j8K9gkZh#(tbbc;X1sN&E7b*~*91?^l`XO9i zh4A}-76_LI2${B5AmnUu5Z?bA^FufqAbh?8;fWzZc&r~noWNa6^}~X&6(GbvQGt-N z#XXLo!eg50Es4f%23~I3zAT zzJTe)feM8C7YQNN3Ho8_(e1x3ScatAUzmSNsM5@)7app>vTu>F%xz}ReKjF5v(IYl z(TDp#0^CE=herp%UFgHT6}a~d1@0u@-4Ef4s>VMk2v-D+Phg^=@i|+Z#(!ykE@iU6 zqB7ZkpaSRaA;C$EY=Hg!wBS5Ly$l1ciUZ^FTs=LBrzTiho0;Qby*eEkA4DBmjzG>c~-;? zXA1+h@DFdF-#7)%WfeGIsK9x0C~(fFPEPbg-@~^@hUmlEwE#EA^A)&HEE4V{Kh_UR zk9qTjA$U(KOuK6VeyQUXaE>h!oVf`$_f@v%?Fap=Digo>AWVE#U}9qW6(;6vkxV?l zrvoNFtHQ)bE6_hT6zG%uNI#rCbLIQ?0p}U&%wn(}pMS8H3iH_tghv($A%Uxbu>AB8 zVR>MFJEw%@a0Qkp7YWPUTsikuVvqfB_sk?`J`N4f&>K)0rVq@oTU3PHjQ2wo&<_p; z^u)3>dK74nW9~l)$RRoAo}b_DVKWwo@X-o{j|>SyR&@262qds}9dr8x;ZzVgr1exp z4rdD!ITbPQ8i8%Iy(<4nk zB5;NzP4_N9n%-Z5Wyd06nU7$s`0SOYJ+|w9%|eFcn%h3W7$_Xb+ba;x3>PNwF~8Yh65$*XM1Vc<23m2&jIBbIt>aXa{v4e5R^#OV{m5|$~%Sw z<*|M!S=80FdwyL|P6t{}_t28F#nJMi`2&(bIbEUUySh-`J{%}n&D%@M9yjyn1?8~Z z%me%ag`4@dE_}Bx4nC%g{gAAz((7*p$;v=4lHV(KDrbwM*8>B`-pUHSZt22v^Wxx{ zTaM0sl__RF>^)|{eoZllX9hev|DY_VS~qk7-!Kfo6AIGw@@Y?I;HL%QkTL_04jhfu z%)o29K&~GSkeBxj#tW(}`~?AdL15tvdRUmV#j)_A`3EB+=Yk3gU)2Tj^2Gs}P_n9* zW<8<1`(^74DRlSJ04Giny1S$c$hyS=GWV#P`)Z;}Y&{Nv9)Ws9@ya0y)Lrv4>O>(> zFX{q4Jq(}|=IXuR^>_o03A{7(1{AvgzySNTa0L*BD8dBUaKM`y1m2571n<%Ly#o^k zcw{vf;7u(KJe7OvrBjcic&9R{hvX=JXy8P@>L^Ch7T~lN3l8OqdPY^x#PIhdO3rX% zSaCuo#*S0)PEAlnOV#M2E90$r+*sWA9)4K4cLzV`UzXlpSx#plvaC)Ek4N(Z z@y6E2k+)}S0qtZXJ6nqv8~XeS-_?tw2|D8IsWR_UBcJjERXMj#Ht41uSwq^&Kne$ zg;sk_mB6B~C6_I7fkiY9Sn~phM6DAPczbK4PSb4``})B7J9 z5$-4GzSWjv8J#h+mM&>cov^18;w?6E{0>cw6Q-JKSAt)m)h z#rpA4TkTBWu8g*-lAIt5-zfmER%KKjQ>)5IB{~79{Xbw1*!7Wb12n z*g>Ty^@o?7<<948;`!~=<;%ahb)PCY!oym}m9apJA`2_VY=aCzukSvHbG>I(a%80t zn``3u-5DEBY4{Z}!*G4VpoxtbfB6!Cc=ez-3vJSDY z%qgF_Vgb*rSimz^F5sD~c&2ZFb%!j-Wq8dnyr#R)Q1p-5z4PoW)f@-Wydy!;o);H} z2}pX1C*r7iZlFv_6!vTL@_C)<@HyS=$%O$iBeGIDT9mu(6xNZj39M@OO^ZZ$JTxXaAU%MCQwC~qD^ISi# z_xc-v#+XVJ7p-V=_h>U*M%o$$kyKVb|7^Y}I-dM{mX@1XD(?J1qB^4Q2Gb|el3LrI zsv^Mirfsy8p1Q0O-$;B^LU@ezEs{3EkgZOYK+R9YVa`nD;YU98Sk$Vw>&{j6x5UYM zb-)Za3XwlgaZXulTDRIxTwAGh`H8sH#BqMr#@5f!Rn2adOfO*VeU2o3y;LuaPL(B7 zU9qU-vjs4v0zwg0`_^g~`qcA{v1NeMf04P2Uksm>ptL&-bn*8`n!8ne*N}&~BM{m# zNC>-cnEsfMo#MEmI)JSw1|7CXcpJEje^xuxHwlcbiYhv%4HBuON>B$j`!=pVy7LKE z3Zo#v=kq_z@i8v6J}J$UNKH)fW8@$p_aiCl>E+$ z$kYkoL^@a6Nd%L+Vl@wgN+3~KS`3HTo>W2e30)8+15uzwjD|SU zMZ=ciO4^UH)Bog3U3_2^#EPG&yHJN5R9F?C)eV;kS}s{nukTH;OY^T%UNqGO)_+qz zK2JAZ6h--OZMy+LM)RaN!=@SGtUaQ-!XrGz5vJ@YR*gUZPKu$A7Ka!)+5l%H|6bd% zJmO*a46NYf;Rod6QqU!pvuI;(`!#+rj#lPoGGrsycScrWto+(`Z3LH%tZGsSmBJWu z8`*IER*hb@4-x;L0C(Du$%P*GbW8wE0T@kZOR zjY|2jODe1v2!E$Ir#&i;O*OhG0NfONv6Ya6aU49TW=UOKIR_ZG&j!OGo2Y`}kTt58 zHi(kgd=xbRG-f1Jbbyi_I9zqGF;CIsXoSrOwm@44P`kxb zJ_W*Z&Pxd|!SN$ox7e%ixsJAA;CzA$#t-CRZAFWmj5h1>w0wLEjN_Gu0l#Lu)&m-x zLECW5One>H7f_=dw(OAYcXTpSyAB=^yTc>U-$2oZ1ohl%zSQ$WA1F{*pA&_5%toPO zwcv!EU4B^;P`F2z9IIImRz>x?{MsrzQ&$E2nh6jMS)Z~+R@kFOh5Y6UE^5SUyt}&b zD^&$_awM*AD_BsxuYa_~NTc{u)M;q%EvnOFp*7ekxl>=2#TzyEwDFW*zzzx-D4L9U zE6K-n2R?57Qlg!_DK|fE`=&tN!!Q5)>nGU?kHe-m^wRHjtJ*Q&YHZRVl;Jo6@8>hg zbz6|p@g}g}xIUwZ$W2?eOpdY@-MGpejNtO%V_d%8x^axkEA*5N5%Z&dnANob0Xt=} zXn-I%t~~*`8bKHv3ViHWOlD^A_8=4Zf22qHsf=vXPtS z9h~w*!|bAA+LXNlKa|KZ#cmb-Z`A|m?qLNR+sg7@o?E+?_KCGd>SfY)be{6O|_;WN`f=8{-}0Lq+}`U2)pCnh!YRSUuE?*Fm$riHJNc(D%$Whr1%6r|Vjr4(yI(j^UO43wa|1-dA0Z!gD|%Y@9WUZ4 z!wdSvYLuG{^{vAtt?tg>XY{5VO}cp%tp7eXQo)QdFcSzF17_Gy7|#n@yY(m(!#DAU z$fn9tSdmSUWg!%UwH%sEHD1Ta&54?FEr01tNA$dgGz}Mny7l=Hf2wa|+rFositRnU zC1#{6f5sIIXIW1)cfO_=ar4h&_S8<+qvO}PWtmt7{pGg`8gm@!r$O0ewZf3)Cu*a0 zRM(`XN-|aqWsPhV)w(jTgtmG`a#AP-Rn|dep(b1J$20OEf`jtx6AUPRR(9e|Hk<(u z{f+f=#IED5vfI9&91pUrbq>?VoL(v7&`qD0WQu7E%&B0OJye)w&E?E*a8!WO=p9#J zO??+=C`BIdKu_2a;g7%*X*)?F@RA(9g-tKmcs=6I=!vy)YEEn}<7>2xcVLo_R!*mS zPgnJxuGT5teUv?b^KasNVtm?)^Z))Q2bmR!_;z~ei^b_D6E+e;py+e$2ts1_+CExh zwh0GwZPbw~Vr`Ux1Dr#dpS78P2`Z%TmAb`a#`d$GJBMc?9x)StqGN4z^n9zW*J-gu z4`Q{di3!9K&*&C?VAc}~{^MYRiB_0ByEa~1yfwoW$&#yGwmlme<_=G>MW>*)V zH(1pRn@sSl*`hWFYdxc-Af+dACxx=|EZ@(MA+q_;9aZszP(0Fhq}sV5$CoOiVu|Cs zd$HcB$^O={S<;A6V?TkyFp}vDm|1M&Wc2-5!fPK_08}W(q`TZ|Frc;KTQS1J% z(h(!xIgxg;{{Pu~@4&W;^MCw(w!C6FakjISJv=i>%(AkGvq?gL(${axNKW{Cet&#r zKX>=s-Sgbu&)wS`Gm&15ne5Yz!WsFd&rW=G%9&A!){Aeg-E$d`QF6bkzv28Hv1bj5 z=E*1&4p)`qo$KMGD+lO){D=yDx6oS;@=HEw;|8YjNQA8QY@#qXa3XAQlFMG_o_}y^ z15f-cv>om^O;*#9PdG!FCat3L-s55AkXHOMwEPqd!qzldF8r`?QQ;#Z2sSE z_*|JBv#^%W;!Zkqf#2h_H(~OJSyqPm5*VxDc#JIa2uH51Wlw~ul(~FFG72#(M54H+ zMa+w!aZM*zhN$*H(5c*o0p3=X$>}dXqbVlptp#SD^-pf!BkXYz^g;4#KaE?^M{&C( zlV4Afk(3)020JVPUl{bvVUKQ{F-S|@kg#WguLWYnB4;@jWiq*sff)?45B{*Rfk>eS zB9i7o7BXRc9^A%fAjTP((<5u~H3#rwRYxKd6C(ns*W{nm=*Z*}3o~Pkrq=jjw#diQ z;0yC!oRR4%9cxC8j5Ir8RNy>(#a4dLm(xR@K}p84vSQAjiDjZQBi=RxlPzavye${^ zPJScB!U3=DF{5PMEV+lpqKsK4zUaj~lRg+L%yH$AfJp{omyOvoVwa0mF}GE+rf1ku z&t*+T0T0oy4`%5-73Q2A31q8f)iLr{q3NI+s8)+CVnGP!p)bu1Pr(mrVHkr|x#~J^ zXV>YFR_rJF=^LCfnj1m62pyF_H(wcs=I6ks@K#&C&2WyPl97)<@t=R-KLg*$Hn2u| z$jJZQqJY1t-LcNQ+3OBF+I`+8Psrg9INUCO!0&UrnjELBTUp)^X!e%3)R&h9BcX7l zr92!6G`SmGK7XiuX4!&SrCaCEtC=^aG~#dc2ip9lO+J5QYiYedQr_gNEhi22P|z*S zv<$j}?d7hfrhr>oy8}&42;Ua~!`g7r>n(3~wUm4P;b41NFl0o-ulI!;BDLk=V8qqp zD=%|5xgs8Kd0QaZNTN`=FBFP+OBc*5WtgSzK+s#-5(tLPVBJAi7=9EnV?!nkB+wEY zOqtsitPhk2z4g8jq9CK_^LxBo%S;|+zCh{B1#=d->*l-X)y}P*JNKBn@<_0$bXM8i zviZ_`Lm(7p)OA`J7wHgc^0{GRtU?X0px09#YDPxTA||OvH>(o{A9mF?dCRv%eD21c z!S^K#>LTGt&>Jd6)$sehO{EoOv&&}BlF1JEy`hFcOqPVgNIOa)+z|A-Jmv04Fo?p4 z$%L<-Gb!OSDxw7n<0)fUzONgmqlPiyDW2GT)u+N~Rx6?o19SuI;#O%e3QM{V5a_IzPi zsIk1J#^(=vgMQ?)D;#L{xog^7jb^C5x0`ye-^+2!Q||GFT3lgwLn>6Uevhy`{Jvx@ zj8U(R)YX|$>QOKJ-mr{yldHYvFy4GEH7<`QuAGl*t?%yS6HYc_YuHf@xx1Z&gXJhf z)&mCDd%=eywIO%V*TNq6esQIf-47%{JH#U@`J9pD#g_n|chJyay=dX_m>MN)WA&k-S z!Sre1U}2ZmVxEpZ6LXYVWiw~Zl%2eHE1O)h$eZ$bAX*Ql{Iu`gA@=Z;{F5Zz?JLVZ?Qh3p)-zG(>{Aar)# za(4^0nDD-W9BRSBq}f#;8_rkNi(8Vd_E!8za!eBY)D`+Xei!D_m`%%kY>D{WnU}GQ zk7}=>2ST}E3i#b-Sh69Z3`6A&u24g5kQ-2#;LlYn!JwR*HQ%h9ao+&x1LFvr*1$00IU ztf$Q)|BCXxJG_Kp9Z8DgM`TRGysqYQb6}2bTzv^9c}i*9cB z#W4R0x>&CCgDq|uOm`3AY2sdMz->+s&5ldk(QAB_mY%H+-38B1C?*ko>Nvtae>phJ zjtXb&V7f7xEA{#@;-ST0Ix)M<+~+JSNs=FB5kv7b!J1~89v^2TG*gkG{VC%{o?2g9{ z@L6RE_jKUvEqF8VQ!Tg~_~{n>B;Za9z8Clk3;sjki!Atiz;Oy`oK@#)3VCsI<@HCJ znzkD@*cAeD0a<`-Kn}oaPq#dP&ciO4nP=VG?+z|TFb#FoxqMAtPo=~7WK^Ls%0lx6 z%+~Z4AwJf8pAFoa-YIeT0pN_6j>99sofiCV;7ngX0OL-`2iO%@?F(SnUxC$r0PL6- z1gkwneq_ zfL)6MtNj+(Vd#)xwGY8AtiWm?fnBQtt9={nwkojNx5I9m0%!(hh4_17e4*id>k-Iy zbd6YME93_}WqoCP*Eq|_iZflb>weXhh=ncovFM;{NAlCIOxWpn!we8BY;R#So8T($PbMgCU=MR?8AX=U56V> z26AeH{6`=XcBq{!N3%EV;*>=5S&#TIU5WU3d>%R6w*&*8h}-LMVFKXknZVkBr`^!} zJ_IlJ88IoKkDhcmKy8ZBO-Fiq(9uUvx`@BU=hxva1|9QL>8NtI zXJ9Ns&Sy0Lr@^}_p1^bBM)o@>} zJFb@?oF(ybRbH3DrzNnM|44nHRjpB#tR$Kj_ayc0ZMLB2A+=K^*E=EIHnc>tz`^B>0l ze89H=p8)9oZNLQp?f*iUzXQ^Q$>`CJLO3eceNA40zx;8FNB(60q>zC-`l0d#)NQ2aFh zwK#lG9KKND*MrAm#qS2_R{Tf6Zv=Dz7~V|)YkGbR{3n170J{G)4!;@rEr4YJy59=W z@#Fm1nqTL};mvV4^Tq1_H*xr)IQ$n1zYV$$|7S4k@NZV-dpqoO{O^F->i_2!|8K|D zhbI+oEx$YA|1Q!13IUA6-GCN_?}KjTe-CgSpG}#u>8p;zzZ-{dRrtN&u@}Jn{UzW& zz&{m!KlEP#Iu-r^^alZ-Df}Vm4+A_3|26bS0H-N@KXe_xYY~7o|4)y@%i{3Gz=v7r z&yRE8qVPxIXRQkFG3eI(dE7$(r?~Y0L*Y-r56ckjj3)t40XhK0IUm#Ue>9H%R)s$U zKVMh=S*NV{Z-GAx2rKsk&^5o$7e-1R<=gEcsJm7bL3WdJ_^NRr8pS}e1 z%YY34#{U(74)0&Eu;%}RarkqhzXm%y0I2ag;17T|a2@pz zSn-^ni-;O;0$h&8ke9>uUHA5$oKs*phHR%FYYEK%y}@80=;$8Ao7#$!^jg5A&%OoTD`JL%XwQB7cqh6h3@lp-~U>A^>~r zrPwcp7A>#HCjeU`>)bnlcLAD*4Sr@>@IV}o1;82SYql2}w*u}2 z{0i_G;7P!9fZqfD2q=KRY(PIi9v~CIaA~Gad$$5F^c8^B*g*IC1Cja$2N&XwaKJ&S z8jsh3^9yV*SNl=-vGT6PjS(-}L(mK1hY$`=)C|81JU#)C{|A7BfYAV^^$?(9t50!g z@NRW9dHwa_28&H}cKkdD*q+!%`NO5N_ufU1AYk}}hU7bC?Sx>t@kmNz(FcCLtzx zc?^#Yu#XWxmMJ04VL)euv3;&twq9nmj=4k}+=uY_ePJIjU3fipJ_wG}un=e0ja+1V zL$P-2Oc}6R?T)Mg)2Mg9w@v%N@t3q-1OdZ(ucubyi7lvZB_h)A0v-1j#5h$Z@dy7|gMBMDrd2RMgdM}YKXip*P z+~MJ|Aa&v!y+Phrp_MXPmiyY#?5A|`Dz?^`>5v#1hhg}RG2^DAV|9iACKIEbqN&A} zWs|Sj7hdOe;U<8md+Oro_M%#z@i_RiEA7cd`Dlc!EcbTYO%Q<$g%^86IMw#J{NYvJ zU}KYaBkx9dqLv|vhb$ImMQrr8`&g>78@D4owu!$0{tCdv?6XhMqn*Qk`BT8rnwQC9 z2S1Ll43G&Os9+@uJ#Rk7Gr)8}CBXT`Ul5H;PG&ql1N;pTKet;Hh%}jd#A*oJ3#YnaR!q>lX;Rx0qeKo0QzAF zn&S4O-k`-*vJjFZFLTtQ)tX-Fz2O>VPGE(55k438cojRu0EaLd#kO#uP9x;g$TosO>F?znqbj{X3(8g(h9=a;TkpbkQRF5TW6@WbmJaUpjrC^W8BpZc5{-_?A_AW z$sJS-Wez7GLcgmecX4Bm7azM>MIBJo@W)wB4Q5F-nCg0T#JVkY?6pe>$$%ucIds*S zTjAP|>BNGT_EaE#B>hJ%c_bBQg&d)5hiS`Isw6j2c}Qfz_34Pkiks_ZfYE`aI{*%e zHP+LCI42Y3Bqq+G`$)rdh;F1?tEuTEBVeh#QmVW=zuC*++I9`{!q*zptRj#)tnqnt z3(;Kx@1FW~3($2s23Pf0T@bFRbv3aU><&^pFlR8^Fz*rTu12G*Hr58O`&P?UCruJx z!>qNh#udR4K#+4j?1rHaEkvKGmjHW#7ZJ@sr;Fb$8nrYg2hottWr1P51bi|L zxm?K^#^)OEhfNOr9LFmEcJ|CzPXd>Vll}uFzC`1N9AUBB27DrPF=+6Rq0qN$_hG~7 zYNa+d)+cEF`+^cac10h~KU##rtuU^ftW84Zr{RE;Ue6-tR0 zV**3Pi45>*G)$7IGJ?Qm{!E!lTVp418PjRgC4Mn*rbo;uE-8h6qjoRjiqSCk0cScy z#mrf=p+5tB67e~6=N$w66W~mVn7^R%Sm=4jtMnYlT?(8=0_RSaal(mIjbnjJ|5eU~ zi-2zcE^~7666x*+&hU(-CrNxOaOQ_tR=u2S>WekLa@FcJz;Dz3*Pbl>KLT8q&${&z ze+jq@f5RyfKLlKcxA9boe-2#c&uOPiJpTkm|FttDJ{GtvsjqW)0w?jnW%+!QyAzNi z(e9gE5^vOaty|)q+P}vu@vDK$_|(-){1?C(AJO33+z5R?aLx&gre><6VQ=em|!YZv9XA*|I z%3AB?>aVggy;OBm9&xH-SRK7o^|83j*eK$ol(ueeN1VJxa<;aXlg+*)uqXMEB{3nt zqnk*xb*EK7V`62}AH`%w86M4aI-?VdBP`Y)jE-8$6LC%}3%KlEk7dg`SKHwd+0$$4 z$w)}s_lO>*{>6kAtr{urB_t;9Q(V`Y%e+)GHr9LkncIW@$ReF$CPzc`yLrb@cc={pR4 z`e{5o!?e@cqdxR0wJCE+FU{$&?Iq>v@jA#D2fYqFpo1A}GtOQ7VWR?b`C|Yl0u}>S z0M-C@-_{g& zofB_O9sRv*y?OP2yOXSA!EMIgcGbG~HB;N{Y*AZhNq5+S=8=W=7r&>b?aht2(Ektj zyLFtXbMHI`NA$T*9^ElM%vN&uxkUGo#sBWPM*K!)^fV*h!ENsFC)_``x>Ba{7Kd_>|B0T-MU&+5 zdRy=YY-5siB&EX>h~XqOCwarCU}L}9qXJ1U(Fv&2POX}@f>kt~Np`YWH+uL^FT#n) z(~hwv)hR4{diYK+#)-(&j zY5&*K-}R(QFCj?-2b;80k|0-7s3A$I(o0EiY|>6iqHIYaDapywOG~eO(oW2B^J$({ zrab9-tQk`2dXiG5mz3Vv9BE3l#6};I6GwVPY3j+VmxN^(m}0~bRN-6QvDhi{HAqJ@ z#B1c$=~}#u!bkLQd7=(aYC3$liLI|wY+4JK%F1;RPiddF8X{uTH&qWcW%-*h@8)@K z_TWPK0>Be6^E_7LSHsNnR*0YaLJgJR$-ChWycbV-JOoO1aKCg@p+SiOO?Ly#Jm=SR zlya$c;W2v)>3V#*-~i|x7P@O-Cfz>(6#(wx{1fmmfYt8b%I<34Mh`duw(S;o&|F@z-pJ1z>arh;{D|%u`8?Ky8MtnU0W<`J4^EjUA7yxr2-~n7ip&wpiqfmp!9{>(#LvN*NA@V0`%_Dy8 z&j)|U!Jk^H%2$+8UA@ivc7haXuXKC@ItTnM$F(0n^xuOyMG4|XLkGhC5bT-HgJ2#E zQ02U7jpVMc{_p|!K%2e*pGaw(cq+DH{|`6C6Kq?V?JnNIy6qM3 zX6|*Rx(}G{E~~j${r~pulkeNdZ63yMI_j2Jf0)hhn62GpTfK+d@cv&HDSh1&D^hw0 zxK*Uo=2f^7DIE>mDpFbooa3_=DP6B|B~rRo`&S~R-vTa0{FF%PRT@_!rMCm8aHke2 z{hfAKBBk#E=eVpzO1}hd6)7E67b{Xa54csNbS-eJNa@+YInHR2((8avBd$bBp8_sL z4wXph?}2m5Xeg1=gTR>{EmHbl?XE;h2h_)kluiQ9@U%$jGT=;)7AbWBm+4U=rDp-R zij-cg{VS2uTYyWEMkP{uA8^hmv`Faz;G@W2iIg7HxDqMNZczS}Na-lxvV4?CX&G>< zNa{!Dv{DRfpdvtD3Q{y6&A|J`VPX1^;F}WV_I=uRkGrL zi^KmOhjaNt|9WZhelwO99r$!<{XO&qtW+2NfC=8GaQo6QGZC^!0g-Ukf+3ub%T|Qy_J1^ac=@sC?9{ zveFZ1<`?V|Ol9L~QeTcIpzLMwefZc$w-PK<(F76Z4%}jS*Qq<9iZu1O_s*mb$&e>o z%djE9OQX@0BsnxOb1o4{>M7`z$B`sp5tj66Ccr|{=LE#^IFbaM!dK?fOo5fA&q;`5 zawKV3DxV%qGZ8UFeNII@mm^6A8-zEQW-?-k`kah-E=Q6K-dDi=vNY2XOV#Iu^k8!& zDOsfMnWveQII=#cr6-@hCT2aZ<;f0|*VfZWO(MEJB_|1^BS{Z7sW9~MwZt?N)Pu0k zDN4lYNRo7t`K%&u&!?HDo|Jt~R6{F_evg&K9*jq9=Yfu5@ ztTl~vC8OzE4w$XN+*Bg=I(tpNhll8hrG@K{2q4atn=i_~-e> ziO@AZ3pn=_@TqJrWq2~?ST8^B>j<~u*@3~C=J8hmer5nPz8Pk&Ew98=U|ic=2ABYd zdEUT{E*#H)`rPRM8nBA0g`61`hLrJ}?Efl?! zCbF>6o2Z5%!1h%uBG=hi{eR@cYa-W8UVLFkiC36EqFsa7zdrsY8?RHZ)+pWPGX87qzLx_R?8}@|i11gymsV7)!+_ zgb1F?@YKaR=xg!~`H=!#+Q(-j9Gf=j4@qE=5NPvy)`ADmvQC4}JqCP09rXy0Y6Nj^ zgOmj2x7Jb7Wb|<4hVdlPubsbH744BxV zb1z^l%;NxftL2t2uv74_5{BS_aadJ+E;RSb$_tgK8TcbJG^ozLAsqJ$T76A?Jg&)TQ#e|8-QEW5;SIVtkPs#zAM~x|ZPrX~{8AZ_c(n0jBGu*Tw7gO+P9o`m+4rBOqxj)HX zNfHXaSdktDA2Y#Y6TJ|vMFgWqwDRjSz13=+oJ3TI4{M#b9#5!vg9`)QGOCBkg+wHW z$%aHE=!Fw!19O^Hy4OZ#dI;gQiyHa0_5|74&8HdLiOJ5OyM5ImOuP=1uspkcB^KAF z`zo);=fatOLV0d_Fxxefd~5n^`zThsIn68mE!XTNs_O6lFCYxU80T4 z7-qfnYPuV!tVBPs*_R~1ZeO}*O_~DTEml@yBiH9j#!z*hy;cS@nJdklNcpj>?vzI< z)9Oc*FocqHLlceEHsLGnm^UU!ORVQ=5B8Gz42@hQ;6v{o@7C0a&}i^jmCu8r2txGU zU;?Ujcnfc(n(r;cjFp`S#Ea0{)GJ*HXxNFUgg^>0+8?%?*5_FBjg zpJK(UXYKMDP?A;^AF3u*2dPQosbUu^JROuTvnHYFPFbwja>~i;!5|t_ib1CuswTo9 zlcb?pGNh+iY-MGi3HecOygy~G0eX*aqHt5rr=$|B?19{T>Fh9>6%)G7F9aW`OAu>S zDwUN`$=Xh`_U_dnHCGD%_dLX&zTdS`M3X;>*A&x^_`rtmC~PJIH(zNRNJtD~2y!&S7{ z+>MKkB0frK>*m%=@~YK4+mUeHeC(W|enD&*J6>43e;sRf3TacaK;!((26wG)$}2Ru z!}V_fB{Q`I?x$GjQ({Boe~!ani^HD+zQ*F8pPwOpcZ}7etoj+QN{4aJcT6b@^tMv) z+mux2b@8kz{qwx@mZ&O?!-3x7rgJ0d0n^2hBLE4;;^67LKhWRY!cCh1C3rko_2I&(%IB&+%18bM4|TP z+CZW^xQW8ipN~tBX!WSG*At~n`Y=>aucgbx5T~^E(kdjFz0@jef?;6m|P(JFm$OdUsOoz)T_o$^9^TY zMl=dZ22F;4IO5ccHbri|6t*z|eWKo_oe1+Jz#HfjCqw7XEny1GQvnrQ4dbng%`XG! zzKFLlC{?KOFJNXL&PQqGqu(&|Q&&~cxdYA57R~^;c_kf9i?0}CyIOrP#UJ1{wtCsv z>ZMHbGCNQRbh#XL5x?A)w0hG0E15aAV}S8{N*pF3zwRb~_*TPhKl!yGKiY5)4ZAWN z_-KQbO&JLj1jVmH2I?GmEU6x^x9|%TSS2|Y1)5v1TXI}v_N)^OqZr{ZnanosahC$V zg^MP$fpduCp%9+{<+Gpj7r1KO=&KiZ$3SFWV&9m$CRww#W5 z*TPAIX=@0i5XchGthsY%E=VWoR*F;;zdo35FfkNKgW=={G68swozICUZy@phk~foh zpS?7ec(2KuOuWy-H=KBiWX;E1v_x}&dmMbU*1Y>-uHzs->_)eXJ<3GYGzA7Boxh3t zm0szi1N{W!IM5Y6lajW<;us4Tpa z*!#xvVVNCngOKy7#qzcHCFa}j(bDc^I+utb^@2$jWV(exK3MCq&)5)XIpwedQc3)$ z7x4rE9zndL^Wxt3oh&hrrRY=Kd$35W30UI74l5Fhc2ud<9F={EDf^U~q^$ays)gnk z3gVO1r^qGe)l2L!kDLe*mvf+BgGnWYp?Y2_@p-m7j&m$>u5~V|-f;Rk_&(n@`1>KY zN%&S`Pyb#|y|>KVE^r)wJQfN~bs8;o%+&OED|(IpG7kR)yjLaU9}2q5@Ey5n)5+Zv z3Y&=LvjGn|avMwI2e4R}Xu$`=aZ*CQiQ?^t_?RW~7CxEa%k=AYz-fqo1@NA8M^lB6I1ggp9`fJ6c) zhe<$BlJpahkVjexz)CBvw;PfqDm^x-Cp2xoX(e7ilff5RrfrzM$@1;GI$sbk%uw{F zd+WyZ-lG=l_s*qf$bdT3^BbGEgNMmFZF;O@|KA9^Wd_#7K`3+t^{K35;B~Wa+4(V{Yw?z0ZadQO126rL8 z4)`03J*Dl?dC`^e|Bf)fExa6N?qz9wG0ejOdaTiVPCYVr6T9<7Q|8{zC(Ma`k9&tn zx670GPraHZXOLEXG}FrO!=)bgUT9K}fA36sNrSE@>o5mdit<$T1e>USh#~VKEu73a zN=va-8E`ba{Ou0xilUDB9?_G( zkXK6MwR+d{*F6B)Lx=n`*G_O5SY?n+^xi_h;J9uh>vT?{)?;H*nOZL z)(}{TwX?)WKYtQ;0PeONL3lyG6qg|&S%^&ncF~7FT(z!#*UPV4R#vL<&1niRr6#mz zS+$#NQg7!yqX61ons?9cxF-iDf`W$L72b9TZ<>3fHIUAuU6NgK_BU2e(b=x~)z2go z?SvGrW{Q3FP7%9kXD)~E8DA+InI2&(fyLF;Xwq8Liz`XbCwsp5oMf`a+>7^|dL2kY zomMR%j|5Lm_Y&V8zSE0vBJ#9joaCM9q|*YXG6_w3u}{t^V zB@Gy>7Gi9|Ij}tCTm-Wo%UWROsI2iO;27+q_!i)N-efWKO94D3T>@SEuZP)6?*?wA zZvamErO?+xC;dq|9Tw#9~J#d(3}P{%d9B7P=1qWSBL5CCpZOu12i%#{wt)I_Nt5^)PFCrprpte74dvt)$-o{dN`q zQ()HgtV>pU&VQ`*tT&|J2wj&y)2Qj6hT9Z?#vg~-O8*RS(w_zzUQgqE>vRkKRWQ?C z)3c6}{%g>6c+6w%ekI&STl`-Evo*Y{fRi8dUFX*~EcAE5&01fWCsz7jDEe<&!mqK= zKM+U%%Q*T675ygAG{DUI=CaU#3Y_i5It#*$NG0VaZ<_`Kt++UV=TX}o4d^<%)BZ8& ztZ}7qNI%%sTEb%c)p_+}xN)AV@f+gke*&C&fa1l>Xnz81=rtm6~7<`v_putys{k6s%H;UV`7XSWDGG=lY%xk0qk@LsrW| z8{)>wbPMg7iniy(Qpu*D9PtL*%~M+(>)?7vIPue-J-sL1*|qxHKvrppSIE~yWP}uD zFC$&g1etpR*xmH`d0Rf_!J%YQ{c;tSm_)sr7&14AO_7s?Q_0@KJ8nQl!`Zn{E4ONYqVpz zq!;g)Cxj|1+weKOnjjWaxXpO@a*88PFJv|d%Y#Fy6>}U>Uvn&uO?nYm-AQ+=fS8>J zUyApokYn+*xcSkXLOqEmOD(hFsrs5*@qE(DF3Ljt{E_Y(E_^vh-APO&{~OloItPo$Gm5iY)Fbu6NX8-cIIz_* z@HM&uM4M3Ru}IFNuk|8vj8adPC0}~JSrhL5Olt4lX|G%k6B+JNtT(qlqdAJ5ChmwP z2sDvzwb|N>Z|u8~UzEQbD5Rh$yFQmcWZwQxgF`cf5Yq9EO=JrGCc6!(lNBFP&QOy) z0+5UZXJPUgnV$2xWE3hRdcD|^jMkESy*@2fY={tu1$DTC|M5oDtDZWD2)cC1q}Pvn5@*vVo_PS5P|`C93$D&AXcwYn&gS> z_MZCkG=`=R3)5b%uO^qRGUqi(NTcHeSK@~Av+q7) zl)6aB{8{kkzL`!B_qNEpS7Nsd0vx_DpLnxBX~s>{-=yd*;i^pWZgqRTcz&##cqM4L z%cE)Ul(f!^E=I}xC!Zz5UHDXrLv#2!X!z*9qR~MmE5vBXw<$XHR()@|y4bv@gm>%w zo>PM^+*t}9x8Vfrbm3V&zJFJT5^%WGZ6|ycn*uL5@8xAE3b0V{0N0+dO2WM-uixRu zr6}H&YQyTb%$WL=d3Bg+=Z{~AqFl^Z5!hZ87Nl3aGLCLUG&tYaxp>o4>9b3 zBETfTmAERs5!Q=PrJ3gRn^N*KEH$PmWMX-+q1| zDKL-AHq)i?$#MAE3fB(@E>jN&%EtxULZP~Fi=k=%1;3|)r^Y8DBFrzoV}(}>sYTr; zA#a&i+V2U#ms;=^;Ku^j;pztq6|Ztlmy3!v$3k}(@Cm^A@qp3};{c4zj~`p`LXD$r zqwbnte{9=Z`Aq~q$-;lU!Y9x2>i;E^XW<_x4Vupk_?reg)1f<^mC9cGI|+D&h5sht z#lRU~#BVK({B;n&(1L6F#mIAO{LTbD^HGY|G{{30$qUw!7t-FLY2QK~bL1+=D~;Oj zDzpuLq(Fw>VSEAqJj}QUH!rWe8l%7=IX+(vE8SvoM;Twb)1MC09}SBUQ;c$-iPAX{ z+;wP=g3n^$l@_~)5rf$l`~dLdE%>8I%0$wXmqqzJ2|J$4AR$q^7huOT9Ua!oz*!=w zi9HV@#!C@>{no17a5}?$8?y$ZN{uUoSEKN>TUpsR%FyEk!=V}u57Q=q*#vX})-c#( zXq@q~;=cmU+Ns?i0&Xq6M^qfOJ8{xuKd2rX>CJ5V@~dxU=vRUvZ;g560qM9Jd_}IF zVobObph|`@Jz4f9Lz16&X2hi5_jh-*L9PIOW7LDHtoZ)EvufSRBHU27a_VpSc z2AtoT)Hr2=hg>3v!i~Kjf2foaehj;wY2gFPHF7-1B_MT| zhsP0R4R{ZvM#j`JWlA!a6CCSTIajW%Sy{cRdIQ3_37^dgK>roOV%uJ%rtWK$R5h%k z@%ah-IQjMinv40aB3;e%WXAcYa8IWnK4nCql+B=w14m}J3Ol-vf}Qq5*_nA3{CNb} zA9&CFLGC+_JD#f}O*;^NSyOxZl`jh-8{yf)dnI}a=0#8cY|ejrd59kC;;hQE;>9B`FPLTQ&yk8XwBj!HA_}YZI$!1n(EbSPuWnja>?qG zHY@|rD_<%!PKKVUgmR?=vv<&N_BnT?Ig7gw>0y3r{8Ho(>$b)Z0WSr<6~KI_nV{W2 ziSs`kmD~I1H-ve^T~bsMXoavS&kIlDz=xa@fuUz6@r%E`a@#>{38EXclEp zgDYuCy%6g2(%19ZOS4NrJ3G4!>Rd^)05(fNLHcRnusqud({ccPmP)lm+LN{vyy$<4 z!nFH0?b6{E`)iS6|3-e?N zN~#opv!yH55^1IlQ%#j-#*n_NrOLc!Y?)(aQZ>ybGRKxcrC+MFq2Kv{Iq*9N`7#?Y z4|Qm+tYtG{HxoEZsv35zJB%&k$M9LBm|CW_8YyL+U|N~d>g=lQwb>^_T_e@iP?v)H zBIH^z^kNx{lTc~|=FSF~sM1^pa|!&H!9SrCP=cCOihL%NAjA@c!hB>dmLO(j;KRJz z025V~%?4>x4bv>ZOmJkaU_Id9EWk|Av*j!YR0F0W6qacN3lxR0mZl&XMYDsm_urX=h2=nUZ#< zq@9VL@m=ps^o>+WI}`mQRnpExA4!$8GbOFl7&@UDBLLM?jarNgtwzWQVw6}X$BI?> z9D1423|Nbiq8T&9Ca6BC)R^ZPEd>LpOkkXUzi4Tvl5F<YcV+;J4VXQKKlGzAYg}5{V?$10Gd`?9ih;IbG5%@;n8xe~T@DT72 zaK_mMjzcGekj9e`vI{Yx+9=g#<y(Sxv~&;PVjQVpA-I^ z@aHsUGz_R2x#ooS{&n{?O+DS+SXy_%hR`<)Ypbyn=7I0oQk@0AZsg!bl#&anbxKzk za+s-{g;Kt^WfuHWoh8+oQmuf>8bWoxRF9GBJgLr=Drs3msFId7geqx2u6U&4iHbuN zpF+K(;xg!mD&DF%Sn);0i@*-z@A-<$fqhWX1)$%974KDC2Y>It_MlXsfX}yq(e3Su zn}Iz6_c!7HS(xeT&5EB@dzK3TXQ)s6VTCP{!v)q~=qZ!bcFF zo8b4witA+xUaYtlZcOK8pksJU|D96(nZm9Ef2P$;-63gW9_@z>_4{Ppo<(~0SNvA; zdI~x@9;|o_>ORDR`EsxH`7rPgK=Cu!{Q~;KaCr+R=EgyU{Dn;KoiIOz5+W6I>5Gcr zNca0dbpvoxcY%&Me=BgaTxrrJL$mgED|{bRvADJ3U4;Jys4lB`TIno90!xfK%kE~V z^mnt$pPLcx2Nges&0A2J=RcONUqELZSsyN|xUu3VaARpb4E1T~jKg(sJt%7tOXfQK zu>`J#Zsy<<74NLRV8cqZ$Hj&hZNiHdQID2bkD5En$gO7$rdk1Y0boAh7{ENhT)-T_ zY-z4Q%UJ-J4>$%e4=@+Nzu5q`EcT%lz!m`J1C9aAGx7)e2igJo1H%LR4|EOOH_#a9 z8#oB2fq+850Dv8k56A=L0I~sDfJ{IJzyQofh_mJ2EVSkg#uCiFKi$DO_yrsGpL@qf$E=DQ@v{))o)0uYo-78AZ-eypA)2gUOjDQG*Erl zOLg3-RG<4E)oIdoP7}?IEmU)*jZ?MW_wlqzYbNghle zxtRKVr5~g8c}kzF^f^kOt@K$+pQ-c;shiwPUM44#kIBX4VRF#)(~YtT{Hrh)J7#01 z;*)<{F~c!UKA29!o-+SVLmy(As$rT5iemut(JraZmMU#cn|aa)dy2&lwp^;Sq)I=g znX@~FOO^c)^#zy}I1$zc%m>)YS7N@k3j5Rn+zeWZ(Y@S=NR{{^jOyi>*HCq1HimhP z!~7eMvx*aq@$zr3gMSVqH&bL%HJ}d3TqsrA43Rd2rOjaJXRx%%g^kO2Amh#fTLyFt z__{EoWPvMfY)>f&uVOA7^YYTXgt{KpP*;XUe7K{p?@+YFNkiP!O=FyQ#QZLCOqZC~CFXXC zIbC9QmzdQhW_F2+E>Yek%DO~pmni8H#a&`Xmzdrqrge#_U1Ca?nA{~Mb%}{xVnUbr zonJif7ti^{0l#?GFMjJ6zwwJ_{Nic9c*-xH^ou9_;&H!t%r74Gi~WA_h+q8LFCO-b zhy3C}zj(kee&rYU`^9~JagJY{?H6bH#hHGw-7mKJMY~^Y^@}#YX!VPTUxfW4>EfT*i62B=D&lHKL zi^Nk!;>jZMM3H#BNIX^~9xW34i^L;E;@3su;Ue)+kvO+NbQXwR1!8A`*ij%l3dA`D z;_L!(R)ILPKx{7%+X_T`f!JCg+6qK#fru1{aDfOFh+u)(QXpCi#9!^=FLv>XU3_d8 zAKAr+c5%oq4%)>BcF|=Q@7u+n?czPVc-Jo8v5P<1#UJhBZM%5OF5a|@H|*jMcJaDh zyk-}_w~JTp;uX7i*)CqPix=(U1-tm2T|93W&)LNRyLi?verp%Mv5RNy;%U2h$}XO? zizn>jal3fTE*`au{dVz)UHsZE9=3~z?Bf0`abK3WH%r`;CH7^ByR*bEvcz3k;?68_ zN0zufOZ+TL+?FM7%@Vg{iJP;;PqV~Nvc!+G#7$Y^#w_upEOA4YxIRl-O*~F(d@mHJp zi%ooD6Cc~eM>g@HO&qd`gEsMjO?27B`!?}sn|RMA-nEH$Y~oKg@kg6@+a}(!i8pQH z4V(CbO}uUsui3=!ZQ@m%c*Q2x*u-j^SY;C{ZDNH@EVqejn^f=wK66UW)au{Kd@6ANr&zD*os6Z33hu1(CbiP<(W%O+;pM1@V1 z+eDd7l-fjzO%&V244as46Vq(i_BX<>6dLDY^MB{TLSq3fj4GqUcnC+0CyH~#{o)I8 zY{rU=&W!yTw#>zu+cJNd`B~;1TiAA&?K4{`_E2uZUJ7oSX0Oh^F#C$^*Rn_CoSt)j z&VxB$<{X#1HFsa`-*e~Wh4OCC`y{WtUvs}(`W@;wJ%3aFHTiGlkF(drWOv$K_A|edzbkR*S7gL_ zzX?5jhjEc{4f^qiAl~to@dXULmp#OvY^a>$-BI-?|m z`E*7`qf)mc&-P~AkZ~7s?uCqZGCo884auC8IS0A8A=8r?MqYj^^YYA}B1fOf{C(yJ z$XAj%~4RgUy3H?zHW--GH3lZ+pS^4)WWUH6&|NR%zDpS*x%y$7 zPy+k19?yCiT-kiL}c^mRPDD7Q&7w27%GJh!VK;By@d81!pKS#gv zekb-@)2{}#;H-WZ_PYvo;r@Ql^m`pO;){Ov{L%T-^B3e-=bw(+(URYhe-Y})?fDPo zA3#m{B;T+X+Q-?;?I+sTpwXw503!XvEI#lpQfxZ8T{?q#}=wFRm=Ih_y|9sT7oBHqT|2PlsjFz1} zR#4{_lEZuO{89#e5B$!+9}c{2-~$7n1pfTMKMwrsz>GnI2JO!nJ*Z^Raf4P4`ud=zL1zxS zV9=F=ZW(m{pl1fXKIqV(F9z8Mj~-ky__)C<2Y-EV)8I1)UoiN}!M6;)fABMdUmtvE z@E3#aLq-oN8FJi^WkXIIQa@zNkR3z5H{{wOcMN%W2*Na;8}inWPlgyn3x_&}mJdB~ z=$fH5_zMg@XXv+w8ph=S{(cI5-_Xa0z6AZfp?@Em4dI?b3i#mXP&)}9mWa6RuB8;u)1Nb!*&l_fi=Mo;NvCw$ha1!cckeym_CuFU%=G)0bO6tHJm@7 zsp`>TFAjTG@&DVftl>k4PaZyZ_>$qL4EGL?4DTGy@V_Hfya+n{HkjIt%X7`Y{pdRk z=Ue2{FNoOefZqp(L)4iz*NNW_e`EMZ!~Z+Hf6>^YvZ51;Ru_G<$X|4J(RYe|Sae$v zy*~i?j{QY16undQS&?nT5PWiZ!iZTT7LHgqq88?s5nIu98%D>7i$+{C;`R{_jW{sk zE!dksR;W)#7$XZuI!0EEtQxs?2OT9T@f2D8qTw!-(7`qm0poqaC9w zMpunS4$^++YCc(Dd~@Y@-`3$XL(wJ6y z!*46v!-pW8GY3}tGj5XAb|Ix!jI~g8xbt&2jiD&f7CPY9j;g=FZaCj3?T<2B#&nFi z4$c>ixq8eMS(oS1%g@Fuo5$Wa_UW;&jXgN_^RfBkMvW^TckH+o~`#RT<5sm@sQ(y12Qj;4;}w;6pSA; zzI6QY<5!LU#`xy(XN|vb{8i&`9sjHGzZw6B@gI)=&-j7~V`CLEmb`Gow5qb3$lJa*!WiDyjQJh6S^o{5)Cym8_$Chnj3 z!o+tbem2oIX~?8WljclXJn7ky8zy-sg(vNrbn&F?C;fcVN)+bFIlrD1L}8Jm^UA-V zv>GDDbCXycLDdnlR6}@xmtOdLWfD!+zwUZ)j`4XGyF@&{fGJNzn%)Pu<+9%VbJ8|R zmpi#=^0di@^P;~ZywV^#%K4L*O+Ib1*`1nV5G4I!R>S12llM&CnDc|lH^TMjlg~t- zKq7oXfuylb^4?*1 zri7<-PT4EtWEgK!2*kKy3R~A*Q})Bw)z6HXR&^74e#+U#2FoaZ{Oq@~IHvoVs%AX;aUFV;d5ABjQ*$wRI|47{)HRo4<=;vS)1v zvJpOhIQ485<(sB1&(Y2Q?y0Q?rz2}}9-I2&)OV-;b?U#T_M0{mdxn#y&7QV!+Ola# z@xT9u;C~}dn^rr9Jj`Fiw6Hkt(BSzkg5s=_AbItk67f5N* zg(?!iM}caV+l}cn4ClB01NWiq+#ZJ8mZ&RzId7xm!n0y5HF@g#rFnQ?=D*<7?=0hi zydB1Z8A}lG{j?c>_@U1E0X@!+pn|p;r_AuqNIhWZJzs(}`&b;?MiAl~#0C$Yis#Fb zi)K9x!i1SeN5_66KSx(I=Z!?ivsDHGs!Gd73#csq@M&N#{V?~MM%V~hViqZ(p>Gm0yVmluDn*mS8YZY^G9 z)E4h9{y}kJ@e<>U8JuNmnni|56YFwg@!iFb6~9#cUh)0K&lJC2{PW^p7e8P8$Krd6 zPsw}*@Idizi~kOx2zyCxiQ&9-C!%+KM%cJ6%WfG5;v*W5vzm-9iMQ>>OG@UHEG$`9 zQd<%%*$ICa#rb8fhK#FAzLO+O!}xQq*|*)3u`Q>YZ($fbe`4o);ViJ4Kr{Ns*A~gI0R%9oaJEUznX zE#F=KgYp~8?=F9={H5~u%Ku)TT`{a;O2xd2r4<`1>MB|*c31oWXWVz=TtBst68m!g z^;=8^MxSqe8-~AT^;T;Wvv>^?=j4Ao(34$e+-tmQ{L2_27KqbDyZC{)Tl`jhBnmQ0 zGgf6ZXIz+ZbH?4UdphG_Mt){-=88;WNAlO0d4A?kaL~||@bBX&{{gndf8%T?+G=d) z*siucXnWK4rEOf+iCHyS?OB&)-JA7l*1xhwKs4_3?5){%i%YZb$<{o61N?6Bp;XP- ze+u*99PJ*jPgE{#^z!|g{hG=}(Szju>_oRx2-N@O0JZ%uDs%w_o>;nGjX#i;rXF6<=N0^9%(zze7Y z)B_p-KEP%GE>~1F0h$4RKmgDJz*@ZuBXRmZ_W}0-_W}O|{`{Z|z&-%`0PF)2H{<PyKLHGjU3xp3H zU~~e`6^t9g7YJVodV$tgb#fOpi}xo_yXYzgby8NiJNZZgYX5yM{l|V&?ykU zq-&R9?R2_~od|Om^j*;RLfz+8;Rhi6@B1R=rLD@QZ<8 z41726-69MeAi}@_0#902$#UyJed%DC?L@uV34IszUC_^kelE%sI>L`2F8~752q%Cm zH75Xir*j*^28eAPyP)q9+Ykl-VVhyB&oHWXsqj$iWVjn(w?~;b!h9;=0);bPe}MUP z*qs5uC38u$31&Bd?mp-ZfXnFSeE%vm>>{8Ah;PCCZNOd1?QW(25A5!x+4=qt5w;CiykP!<=7%zcF&>TI1l%q{ zrO_}HW)obK6y zfySR<{xj^}hW>WOdniPJcvnTT9=rge5jp^2>_r%R!TUA%e=XyGGIl!wVl&*Z7lgQ( zP>%uy2;Aha!h59X>QQlx4$!>_x)(FvgF8U{6FNY+I?i(fpzndc2MzN)G|cm$?}5Gt z4d^^Hu=AkrfxbrsfCHfKfxZWo`8-tS^Pum6zDGoW1EBALz6TWvmDynBMJ0wl2AR+F zHy~XA@k5o~iOPPm(p{*K0K@~GEGqv%yU5+s7fHT~M$_`yz)%h@A060n6RV#gk(pM>c4Rz=HH-PUX4ykp( zIizyfY=?O}UFr#Fd2{Qu@6tT>RP;uBTAg z)?bC=tea8jPYc}MtNIikw4>HJZ5e31zT%Eu9nO95^9$;Ldja(Gpwb_O{v=?aG1h=| zQ`OG_cLMGL`~q+{0J2b3_W+ogzXaR|;FRcBfCm5%0v-b3hQEx*BhdE)9tAuGcpUHq z;7P!}nVrr@0f?7CoCM+|5GSJBdR6JKDg6&he^cpiEB#MOe^=>$R(hAx4=VjbrGKn+rk!bI`WPPL!aSn+pGv3O z=gLg`?J%DSI14b>*5RCM>vXQOVQSgg;oJcI6u?FaozByspAPsM;0!<|{LBZxD-O7|(fQR&S}4=8<$(nCs*XuT6dN#}W@UE$l6ewNbD zQTh(0GY*MbC++$Bw(@tO(!ZGvxAKBfOk=?^OXS?KdnsQ_ZXvU^PFPbmEWjJxA$zO2@fa71LXw^Z`mAsC0aAQ@RgTI<8kse1y_RDSeF6$0>ch z(kCi?veKt2eY(<%m0qg!a;38acfni>a05I5FQ5)k4`=}R0Gk1gfF?jQ08fBa1pqC8 zEr1{(1PB8nfK~vuBOP}L6D2G|b3<1)MZ%mb~6kVfKTb53mCY z0Q~_20EK{ofI)!43Wh)*3K#}JjWC8Qy$JdUWoCE`i|(Y=2~MUXV}6BfpdwS;7>=~KM08@9*$i!XIOq^3Z z1h);N;D0n=48Y1`Jn#ts9iA=&jazkVImmo+-tsVb+a`kEaQ>R+{`pv!+i{vu<7-@`IS_xu`mP0uSk+FL`3H_H?z@S|fxdj2#W&DxE2W=sz~f-s{cML&~d ztUuci`xfm_^N6P>uI=K@@ibO{@qBd+Fr2qPj&SE|JY5}|`A>p=k`6nOuH%9A5&hbA z7_>K>_dNCg5qBPdQB_-`-g71i=_C}DA`?O{Lg*+08HV0_Z=oY4bPz;mlF+0EsTY`m zhzcSQ1VoxN>5<-hRYVjNR8YkCtuyC@F*l(1zW@Di;5%#W-uG#HpJ3M~V>qdojCPrs zGy2PDHp7@{GExqbo}ZYN{mV4;yk3*`19u;z2{0HfJs^}3J(^K{|v5(3bUXi|dc%u9(~mcvFVo$`tmoMq8F{}WZv9RF z|CWZ@p((XZ)6142UNelZ#7laoH0_rJU8^SWyWR7f6J%+q4+bx%sYo(UXKfpWrOq8GWE(ny2N3|Cv~Fxv7eE!Q~vGQ{l6t> zSV+Z~k(i12nltJzkbxH=FPIr9VPqUz(48PxR*uq$^jShI3v+&!j$=FmJl+ z$=_O7^OxGqm=`lnZ0A}w$}F#Xy!I>NX4Kw2AN|O;Pwi*jMo%w~cT}wt zJH7ly)vDT|B?7#0o)EW^z9TEM{IxuzoE^X$X3BRN<1@oBs>RKx=Ufr<-;Ao%JNPlm zwcSTw{QXn=G#~wj=o!lRMC=Bx)_W7BbebjAbF3Ry;o@-1RE=L*qde8c_|o!Y$jG>+{)`mGiy@pzNn5jD?6M-be8%%tJ97g_-!^seVHA^rp}gvv2?TsrxO_ z(nr7MWQ?cng&#erxJ+8=02O%kOO1%>b|Fp+P+qa1mtQSbBK-=f4t{ zPr1^>tXGn6UY@6OL-mL-nm;#DCq&EJn^{?vWbAJ}PDsw7Rc&=C|@(0ix-hS>R3y7LKUeNHfA z7>!?FxFQihMv5mypO;=$N?Ev({j-4tmf!zIwLC8kuIHV*$!thgsj4*51I_DyW#8_? zoI}i#S5wM+9v1A1o3@~avO5zb25)805nY>)?Z>({7wrpOn~io%*WN=LpD6y4(PoL( zKVu$Uq>&)$@V+L=ke_DVUMEsLZm?FDj~a-(FOo<3nc>X)7IT_0&8(tLlGLY@`gYNb zyr0r-r(%0s*CwMq(X~lvGsj7o321wCZ5-MO(TvddbX~6_`;_rd?Y|U#R(c1>SkV8U zW|qTFkU}MDeEWAIn#`W5qC}XNa8h^Pf$%`={k-LKO+w#hc}2wv8d$*({2>6cKp+G` zFoZxTgh4oDg$Rg*Y>*vtKu&l8UWAt*7vzRKkQZ3t8JiDm5C!=m8VW!`C|yNdl0euDe(0DcD3o$CbguU} zRCeb~-15qE;b-NAy43y1^J9ru28S89DXBm4qu;T>~CoDX16?4 z13T0(sE?GFr42q*vG3+pu~H)62u%G5)7w!v2FKw9oCH2A9D5r0q(JOhI0xt90$hYk za2c+^Rk#M%;Rf6U_8Z531GnH?xD9-kAodP?4?nB|c8^qDPr=OFZd$ zWOg=nWna1oe~&G3EUS=Jo!vk@_dv>8T3OBZcWY_IzpNlFz#0!(Q!5LD%(b+#C`jUN zid!?#UiPM8#@`OR4$u)K9f?cU>dHD@Sy?FY%8I)e2=^jL{AS!QW2W!x>vapE`!4q| zmukrRU&)WTuGjPOotw+#Hw^k&aI*+Wp7TzvZ%NfQkJnw274j9AmgAX$TB`z z&i4k4Ap?t$17IAw2g%@!9R#wpk0Bd73>d<(qaXpeeu-T{A!Q|&EZ56RI9akMtFmO# z@eE`#$dWx-pJBFU#-VK5o6^@^rleCwuRDM={ropl77fXHMUXVkfyDEzVHRENMjZMdzfW8?Oe=rS;;t$m5dTk z6XI#2^>aGyVChFVK+Z$Xv-HOd_^XG%dRl+%0sA?~IhFy017spH5t)okX4z~a%Vrai z$w)EJL(W6aK}z`?#OKf=RaInFWF=%JWEo@`Z4mK-Wf1WIs|^!bZ74DsDfuaZzY^LI z;sut_CbEP!5t)n>_d>WAB0d|^hRlV`r47R$u*5bQnT(u=oQIr)oWqLFnXKrXiJXU= zXBmz?5MOq}XV)TC7&1&7;dI%-GJ<%(GEzD1U>Qli0do@OB+QAJ6OqZtWXmYbU>Suw zu=F~S<=?Ul*Tphi7t3;!S(clOoQIr8yOB4EM~!3ot}FxpwSkmXCJp&rX3U6;BOF-b zoGe#&CXwGPUPN9*o<^QV9z`Ct^dx>j zCLxoME~E?hgSa0=?ndrLZbNQEZbWWGu0^h8H7QHw3D=i+fYqeQtmYJ%gp~ZQ#NSHf zN63$mOOQ*D3y=$tvyror(~;AWlaP~;iAY%$Iu$d z*%R3l*#+4J*$&C3AKDRUmt@){87a&2rJYzpO?iLM=dJHQ4Ed)a*CbA|tg*gJJXcuh zdjZbC2{;023U@p1q;Vw{cQ^wl;0UC#zHgFcy&k?bakPM@&?x<06n8uBgl`QkpeZ!6 zn`gBRgx{ctU#cS2Vps_CU>3|!xZ80j%+grgVIjl)MZZ&CFdSyY`y znsPvOr~;Ls9F)fG@7zn`rx<<-z;E-KO1W5clZ_Vv%>xP z?rW@x^xp)FMcw0}*Dd^i-%$G28tW45Za2i8{+j?!yVR5TiE>JxP_ZJT^zr(D>zA8^ zzexCNdiV``_{$BcSG!TK`Z`we6RCb|5UGBE?;30*{f(p_sq!nuCHbBy0JbvCTTKZxw^VTHe1*T~)(sWaH=jT$rVrFH8=r=5; zNAqe@q-iHtm%fObZ?^tZ-J%ZA7xp%gD}AL7(c8ICQ#-NS)_WL-Q6$s>nn( zVM!ATQe}epmgeF6T6$b!mMS#28Op7QzysQi6!z_XjIG`H+@<^0HyRQ`)|N|1(=3e= z!48&gNN-M+lE#z!k;eJGjU^3vge8kd;TRkT$(h7sQQI5Jx^h_yFNhvVr{dk!W^$23 zZpO?TG6^HQJ9tYgZcZk=B&uT-yFxxjegsQl3AkYa%!Ao56Q;uym;~b)LP-t0nTW?6 zk9lMR*?CwfGn=*BNZx4)%_ zD`40c((T^ehTm+f>Qg)Qy`K9}f<8prnKeDqp|URRH0)^_}cR#n~<<8-@y zVpqNq`s>(NgC6KT#m)E)K_BX49)Uhm%=TG32wQYC`WPSg1oT87^LX?LVrEey{-aXR zm%(ya0dK)rNQRlfgMyv-i})0MEv$p}y8Rik4-AaqnK|&;FfmWD{*%c`sb6XC7mQ)onYst2{e4pre*r~qZ)6(|OUAb*35 z-RAGi7WD@W{TuuW58y}m0ltGpBghZ=68j}YCyeLoen&?>W}OlH;b7z-}LW1=C) z8FGRlCmC|GA*UL0x*?Md`JN$X8*;88=Noc?Ar~3aW5^|jeBY2C81f@SE;r-~L#{OB zYD2CuJ$DS$o z5dOPn=FhZ?d&S$CO!I##y$|RY9wr^*!G5Z)f7MU#kfLwT;_*Dap8Tf?@XhwvqEHd4 zLpvA;Q(!S{g6hqx74pwIlCJp)w4F!EnA45A)$icmhK@^T-t5fd}ve zLb~vN7MemF424|~*p2c+11Q~-nGGhuTv!Sl;R@V@d%f`An>vChD!UuJ32WgXoPhI? zwLkBHp*-{(q+*K?Aq}Vk)u9fwgAVWsBn{=UFI*VLV_kS@IQ0QVAqFPHOc*hOnG06I zHuwR~kL0BW+=tiR(Cxv9HU^DE3uiz*=ft&IZTCxuo(X7_={5BiGgn^F=?$k%|L!h9;?eEc6r<`uV}9r@LE{hb7Yh2EPgV+ zYCyVLm!Sl+|Wm%k23Tz=wn2;&pJo=nA(^n4f&b%8Ax3S zQO<^Kqg11|;`bk4SgMP-)&!e9g-JT}l^L~g~(vaU>^n36l z{3Q1F2jXX<^$#(qrS+v^c*qmWgQ3`uBh_{d`3Z6*Ske8!A5t-|flpyAtb_HC8@B++ z0)dbpf*=?|AQZwN9I`?LL_#)*BYZdL4n3eJ^n%{d2l~Pr&=2~<0N@+W+v8yn42B^v z6o$cYkcT5Vh&w0jg?*3)hu}DzhO>|tF2ZGa8S+6CL_+~60>z*Nl!RBH6qJUtP!VFG z4m5-x;U{PVZJ`|`!8n)*$uJXUfg6^9UibEKS7?ya8YfCQX$Hah*3=*J6$pW{OyqK6 z%>@=H){aTOjrZirtu)u*Zdd^GU^dKz=`aN*!FWgnCya&B^))pDhQL601A0L>=nUOs(SX3$ED*;8J5JZCw@z28m_|?xCrOqG@O8=a0m{!tC_Y+0BlgStlnx z!ha5*!8-U9QehQ*0w2R7SOy=$Qb?(1#ZFW6(dPm`8s4{q%)&gN8Tmo>Lv}*Sj4oFj zas?q*5^~icXF|DBlB<#8edu7=ReO^`*^(;}t|XRlcI||DU31QE$VzlJxq|A;8MQd^ z4kFH5#O2S~FtQ0}L+rY9#p2sRzvc?+KK`n}+zvcgg6kvbtoYwc{4KH1OPos0rY!G4 zGE9f5Fc~Jn1Q-X2;DUExER2CSp%-+AkuV&F!eEGp0nm>Q&_WSgOCPa!67&d$KWKKf%9+) zF2fbL3fJHUd=0nYTeuD1!5#P>?!rCz5q<)`E=+%DMt^98tV{W7A!CtM;k9~xsyuEn z=&wL=D1x~F*dPzQ2-zVkgy7$wdU?P%4*rBkb*<={`TKGP0+@$+Cgy3#iI~UX$B7(^-J8e} z$RXGbK=wuUKz1eW&gkuMZw)Q5Yl3VD^`JIxcI>L+uM+WA!mccKWw9#-#W5Fwg6L8B z$%o91drtK1_z%M^82xq~9v;CBxC)ox5&Q!8;U0Wnhsg{7eqezon16?d@H6}bci|3v z3tz)^xB?epEpyNr;#otVJdRwA`7m-nZfUR|eI<6gk-K0AY^$TG&B#gEuYl$75qtpe z!wa|#CB9|&`v`prb{<$H67wP`4>R%eFPICnAQ|`R*iVJYFbO8WI7oz%FboEhFDHye zAA=kXBSGATAjN$EX%E0WiFl{MWS9t@>sY}Ll*9F;$GPH*_=$~^=I1WePD>wjq;Y-*7U+|Fe7q}1i;Cr|Y-@pyH3YXwK z$OFlfa10JZ8tjKXunV@sR@ej^U|nrH%iTB&D2E|y8M2Nc>lsp(#fiU0hHPTUW`=BG z$X14IW5{-f>|n@FhU{XX&2)>f`CP?U zNTvIoptP4LRSC3k)gi3mbx&7uLevC*x<* zx9*wu5*ej8yGj-K$lC&i3|YjG#SB@(kgpiBv>_#YIYX~t$V!HM)sR&TS=Er$4QV%| z!;rNMS;vs|4B5btjSShukj)I)!jP>D*~XCV4B5euoebH}|-thU{m^ z0fvkhX;1rw3a&ibuzSmpZ@$dEw4u8UnP|vyhMXYMKI>P)M@=#8dTQSKm-y%aF5>3B)yxirHt_9WZ1M?7uSf!-hO+$m51QX~@%tJZs4F zhFoCz>w^b*Hz4m3dV#zz_{H!iWh;VT{nL#0Ex%Ez`o1>b0l}*_vDcRai+^9Y%yix{ z{Eq)n-?y{8P7}Mz^7JyZFwe4+^t?|z*F7eDN%1Fk`ZQ!n(IqXhm*3OT{y@UiO*b2E z;^$fEJ*0C+OgB376a4$^dsruC zsrRSPmb<3it$8!YWt|sjvnTzF zqvY{4&;%asPE@heXK+LN29Ie6P;rB(sG+=oOJepS?2q%AX^3|?@%r_{4>yXh5H=r= zFPn0+_gy@VGmgiylW4SVRKAOwOyWGio%f5qiGw&F6XpJAxWW9g2aQLX z5x9LLH;7Jdq=~m9+~v`3Ssua0^CRlSNE#@vN zwkwaQ-zJW3#CHJyJE@aj=a3)VyLIF-D`}l0FImZNY2vB$HjnLbKSo|A6Yc`}N`t&S z^8Nz%A4z98?#{_{I?5&W^Mv$Xr7phVrZ!tw>XmvL#7**x)XndtxsCEICof_6N$bxe zYi_ioiE9_`oyiBwO=Ig(uOAc7ChAfiSx>~@6XL&_K>51!dW85^5xy^Z*$=Khq(|A} zaX&`5!^ylVAJ%wx#=D$=M%UWZdJHz>oS*510>)QNhfzCOjiFX{Y?{#Tnc%Fm=vQpa^^>zC%y z4o!%IJglRPpJRWOFyo2mTvPHwomZzF=TlZ`i$mnO#t8Zrd3=jJ$|LiMl(Q7!CXvrE z^u7CFr_C1*v>FrtGTd*{h5^KXjriZBY=^0vtB^)Ht`V+!3)-7JyiC|5ADzzAMt z&<>q}KMfT-9a_^yUyzQx`Vqg`#(U*M`hb|_SDAMB1C;&96eh?A!FQ?T$53=W8C%4_ z$8iIhT17*D)07O7;C>2n1Da7ui^yO=YZgX9EE!w|Jvw^d)y<+a^r1nf!B5bHj&Kc< zDZn@yVI+mmO(jRi6E`my(0~Q+PRX9F{r)^U?15)u)3D+s(pHx^C%08TYN2tAy zmk4i8?uBW{^_0C2IH`+TB4ID}@Eb@yw1W9?4WzNwLd|wOL4$7XsZ$#K7!7+C`qEif z!Wq(POx*;6)KgcGx+>p|36}wymjT}#T2pV*@G?Q?fz}}JB4tvN0dt;uJZRKqzCqsl z{AeVrHmK9pAf03|bsI38G6H|hF4hLpan8d7sE>cX4rcTEjb-Rs@C7tUaNIlEs`#nf~zY9{|m8tI`)c5>k-fzxiBADZ?d+Ds_ zLE0dMHjsA6w!nL0j8JYh3g9F3XeUcqiaIT!4#k z3vNRR#g)srD>c6LzgiC0NozO2FQ z%O=di<_z$0KZmdbdtbi5?8`CCUWz;T_@9H<9CMj`r(|7;gD-nA3wsRqAqDxW;6bFY z+h9LZIFf;Sm<1l;(&>eH2D6dEOoQo2VT!>dq)AV7FKNW(i$xW{Eeamj^7`q<;K&0T zYVy|xYq3uUI$~}QInY-^I0Qm-?3%&^_z`nmWNp|**iT_Q%!Su+8;5-*qL0hJwPpdLgjXKnWRz-Sl=!(cG&FcA6~?a>D*?Ivxqka)Txr9C<$ z%{FO^-WsGmS|ZIhX^h?wq+RMG&9*V?%D0X(){AdlR3xm_L3w0ZkaEW$y=9lO@TVj& z7X>Nn?)36L?OQJ!<#|NdjD728>@xPN``AgoGwVms>O;9m|5^R$pY$QK?_}0rGWHV} zWz5(|GWLz{$iKH=)Y0oGW1pB((>vy*PNYqx9;6M;vfo9Q@v#L)!X_}=N!mx+LDG;q zpHh=OB(>N>0@tv+0+-+doP#rP3QoW=I0A>j3ZD}`CvrLR5BLPqFdu+@um{G%FW7&M zl)QWiJ7GI~0sq_SWz3J%k<^dO7ryh0uRn47tX7fng&cxhJ!(cEBH0X!y18WF3%b+LbZqNlf zLOX*qxVJ_>iEM#v3XKfvBZWE!HIZ}hXGd0pa!>^-Qy1Q!MODCD7D_`&CaIAqPZ4ID|kT_=5(2;_op$f?wc1+=K7oHhcp&;3`~#^C0DtG981%um{p$ zKddvP*zJPtuoX7J1`v0tJE`{-z*w{|`-qcrd5ii0b4pEKgTlXH4$OpU@GgwUJrTIL zR%0-aBps*c{{`CqzIb1Vab}g6s+ig)j(*tYCJW4utIlviZsv zGaZq><(Irj`K2DJgVclM?OFLLfO}ym4sw0Kz39~d>xSmp%9Rh;kywBUMq%MKL4CBULfX#T@y$SmI;UOc=nGp1?<` zb@(tf_Qf51ocb>mkuXryhhpeL@eGu}>`O_^Ca<9TQVO##r7;UJ24#>!Styr*@|b<8 zfZ3Odm`y67`%)RRFRxSuuc zN(^YBzd>JQUru1Xb)*jnGPXrZUyKInb7p^%zE=dy{%Q6@>C@5|Wz}bC_@}%1%<(UM zSo*K@V;Kk1Z{wgFbcY_$6M8{!=mUL0wk-66{vdm+WUHEN9P{0x_IDdfWX#H#k#XA@ zWQ=x&|E=_8&Up*Q!rL&e7PybmA1N3a~O!%g@GcEMvvf&X@W z(SNf(=sxMmSKP<7I&KNzKc9fJCP~0+xzed z$lomd5cvTtkMfQ$tq_mC^K-WjGvPg0jNL)-jpv{0iYrdjFE1@OaFR~{Rwiyr+<2p% zZg2Ya+RykL|N2W#)1Fn^Vpr5WU;0YZkFQkrJK|qY^RM;`H)6lo5}_tEj!@%a93(*^ zB!CN?@D98UW8p0r1Eb+h7zHC?1Pq5^FcgNsU>F4PFc1bnf9MBqKwszsy`dNMgdWfx zxjUButO|VhiXt2UT^GMo1*htbqg6?NR3v(!)x#=REA1W5h_4=CL76oaBr1PVhTC^mmLnRk4&iIY&wOMHkOv{gM$^F|Lz zdj$GOkVOZhb^B(G#vxp*MN@2ssf3m-Oa!I{FewfqAf;uW964 zDWEp_*JZ0Z5_65bC*vZcfW7?T!WxpqWqfGmZexLeVx)Xo;#|ZQYGsKzh z4|}<;7C$mCF!m)}`bP}KPPVp-A2W<8|3}z)hOENX=p;DIp+hVq7 z1u*R+)J+n-0oUOgT!kxe87{#^xB%zj9GrzSa2ig*NjL$=;TRl+BY!D`?-|6@={bAJ za8t=gx*fgB%-7#Z^X^W1qN$6d2kj%?qNbf0w@9%wrRlEhi}&MS@A;XfDe=qC3};GZ zzart%Ki8iztg_EIfV=tkHbf6K{dh;h5wS}jIi_1?((8ZmL5U+$@dy6+@%bgD$unZd z^vNFqv^&$#bBJH_o3Y|g;#2mu;%1?8(nJYlZ?=_ZJOsKT=1>VFcsy_{{V@Jf!I9EIrJx0D$5X=k z6rS?l5Vx*oJl;@GI~mVMsE5^7-}KCQ%`m39U&dPGmBX|$+O59_IYM5 z=Z+F@s70;R8=dDkv{{JgdXs(qj_Vw3ECm`)0AzJAx>$8;t3*OeZoJ*~p?K7*=6Si`6ZI#JnD2>_K8Ve%Sk!zHnoAwdI)%GWA3h^y zeeIl2KYM1eS)z3s|N5ZJm?8S6xGl7#=bqFvdWukonN1JDSC9q=;Q;K1eXtkyz;5^w zcEL{A0o!34d;wcw3v7l>un|6o4e%MPhjp+PK7}=q%3m^FcvcEv=1VFlUAJ!+w+Jzd zz5I0DeqZdRlTAE_l76AWQ)0b``UZH~nYM`El4j)pL@N7331dvL%g#$$65rp2Ua zHD;ZO7kuIny{m8d^g`*yGYV+vW)Da&gO4BII5O&-axRiiCKb0&?2UX~5M3{inTL$= zm}W-xCHxk1;*oJNg^@cQ-h&x1HIYZVFde3NN3{K+1n4RmH$Oz=EB`J*Ghva>n&!k_ z+5fyuI$>n$T{1GsssW|Hg5@14a61XjSuupE}bNAMwh088P0NP#7=7(C#H zMX(SSz`tNV%!9cw2WG=8cn@YmGR%PKFb$@{6qpR}!X%go6JR`ygCt0V1aN^9-hsDa zEW8C{U^Ki5qhMtERS<=~Mxg@(s1-rkw}@FE8osrena-7x_$_a`{U@!%3FJCqqs*|+ zk_;vB=S=^ipClqb=@ug8fsXI*SrI+U{+~V&n zncfA@`jh8bqFD!pyZIgM|MpL-OB&BhM@%j+t%m3w|JXC;+Pzfr-YfuGVhiXtAV!;kIzyUR(7Ssmzp6Y+gy#cZjG=@nZ zvVa!Z8rs6Quo@1-f073?eM$RS>DI?h(oP0RuN8EG70`@)tU>zL!GEfo^_-dHa^x2} zfzMu|TVjro55ov@CY5kp1^g=Zx~`vh&Bu+YVz2)V53j#8G3$RR_wVw_cF%|SHLk7~ zi+)DG+%e);5`lRt&rJSZ0^-hV3ac3wlBi z=nmZ=4!S}Y=nS2pBXofF&<@%{8)yx!xNl4SjRMXH$gCB_&b)WYSg0Za-k0+1UnX%oa-ZN`=@%ul~?h=nLW}82+ z&pj(`vFAe^HQ6W+U7!_k{VXMI zC^N=Cz2;^qF#hFh0u7-)JNN4#Yw?xgCoVD)?7(D)>e$Rlz(%Wzc1pA-+D!*5WF7JW z5Z}7`r+R~KycA=5MjBgM_^eY~`B+PCwryNzYlD2TXDQ~WUM%z&%*(Z&oQ>G>F{KMj z&YQ6(4D-vFYp_K^KI68WttI!`GRE0Elp|in&L-|w-KlrtkS_$)pTSwW6Jr!zKD?3z zH~BnT3;em> zHsi?Vq|Kl;rS@Px$Y!O(;4Uxz-f+mqrIyeGMgpf2$41DW{sbu- zemK=QenwiU{mb~Tf~*bF8Y3ZR1@9&xb5oRT2J)vS13-2e)&ton)gFezSa`<=PGgQi zgV>0J-DTjE=J*czBLvV3+)x?nLLW$hUm${>&;q(cl7VbKS^;ZeC%n*xI9ihrWHaP2 zkX>j$&^r%d_W;7sCnL)u%fKhVDb?{4@>d9;_sd9#hHk*AmJK)E9Cy(3u&JpCoI&q} z{GlStX&{i1RLy`>vSSze8IWC=_kmNi<4>dM*hRW@dUY;fap!m$@wH;x^AAe*^F zjt1G#^(icc)o=-9bC>MujbOCrfljanPC!#;0xnG)RgsS|_dzDY4c)%ABf1CsJ}cFy%#9q|kY7P>c!7;>)j&25%BHuzFbc*2 zi%1;hk);RHhoD?l?}oU&=qxUAJi=`tvoVWIh`*j=5BjT&JJ~Fk2j<|vK5{IKgq6TT z6i2PWBI`I7>aw<@=n$@6pee98#ou`)7%6NiLsU z!2!rqo#leVC7v3NYPz&LWOs4|7mX1kL{=xxYEaej7{7tBj9K0Ob?mCZYw)Tg-$>fP zKsNEcioeOo+Q>P;x)(>|QJk?L4X*2Ul^n9ElC?0zQNbZQM!$p7b`>j|E9*lOV2up! z<#c|)tkn>^vW`7txS56`y19(w*jp^kLcfK)4r;9Ui*a;ETIg4Mk=ol_jX`8PE7jMxP9eU~O~8ok-XUUjyrS95<0KCo-?XN+Rq%DvK zAPp`6>wZWpKSVi7)MDPo&wY4doVeK>J#?AR(FwOWU_B6i^E&#WHy!WYJ>MJI7nZ|B z2%o_GhLjDKMWG*zhtq_)inL5*E<*2$l#j5ow#acBc?l}irv8wd%Zq(((#qxd0=vDi z_+9UYOxcVnA82JIh+_qE2jrPbzXw)^5atEPLG&$fPq)kII5Ca$5cSAv5!|yoX3e0i z5Sc7?*&KP0m1mMiI0J9NqxU#J&Jy=X$K=_JlX`3>MzX@h@iqFcIkaOZ>Ugf$WhLHl z$H4k5A%ZKqU6|ujbXK4^Lgsllh89Dz8pUx9S*`(BXY*M`1Uumg9Kw#3Dh}B^8oQA5 z0~~5dxuN$WaSL)hL}wKX>1BZchkWd{btCeNd;Ul}q~7lU$d`p9pk^ z@{y}R{xTF7H2O~qS1uH}M!Bl8`dQHJ@U8zA|DFDF+~IFm!PfoO8rE7IE&7oSG}hhL zKHVa46+TFd%@9#)PPU01Z4#I{?HOLS>RiAQvCl{h@Qy+^kfU2MdDn{K~N zw-;S(#C|?{4UW>5qFZ!bj^an`rJsnOjk+JvrMF1BqKm8enXTK;*6l?X8?j%n+ppK{ zMHd^fAFlFP3s_s}N71diZq@CBbUjGdm9BgJTXfxGjn_jp(T}2At)g3X`ygEp(siZl zO4lvAE@2B;HFP<~D@_k8N6{s$97UI~aui*{%CV;&s+NB2iH((QX>t@jNY{gOotDH# z={l{9Zn4G@D!^*jk8vu1Pv9}|s~w#>BpXWY_;^G=u&TT?2fC&>d=oh z@>IxE9jd|W@ESxy7=%C&WC4FD6j3T-M%MLNS7hCg^+Q+!3t=Y2V&4JT8k#{vr~zlg zE{B~BI~;Z(taezNusUJYkk!KWgzgMI8M-I*6r2p*CVI%}ki8*agzSNt=ygKcKs9I` zvNz~-(B7agg5p&cYO0BTl)4aIj`5USbUDT`H%r^dFz3Hd~uBOADT*<({e4O4deB z)qb0-m71V^KTbO~PP-JS`2}jF-_e%6rS*DKTQy4SJwiJ+NLv%Hm5kR84AoBd*ZK|6 zD)!f2@24#+r@dEBTi;vzsJGUooVKi|R-mVLu7_5wyLPslR;Qa*w3{}*i{|X8t>~bA z)Lwh5owmER_OI4j;nv#AEwysZv|P=!pPFj-n`$+iYM(dJmNd~k`L&r1wTcb39Cft{ zb+o|R+6sr(y@poEUz=84>s(zsT20GWRon8q_S!`op(V5vg|%~W+UI4pG6l4m?`w(qwXa{+ZoI6$ltleh*mOK zyPT{w@YnMCYp(@p(H3nJe{b=qCH|;oqp5gp2AVb~ZFQ$1rZU)62AK-My?*fAM)%x( zfTg$oKVO`M|55s}Se(VCE7+h^$YqJiW!ae1vO1^bbN#q6hh=LH3&&LbxG9HaXLif} z>~j2EKcdXgZ6@o+r0kZ>*)0jWO`YtPHrXv4t7W&8jkFYrw3N}0A4FI-MOYT-$NBp4 zoWCWDRUOlh$KTeDbkyoK*G`txY8KV%7u9~Kp>a*&$49a0Q7ZIUq}qj<{phNeb)I%A zbdIG$o&gsBGd-05nQSV6@7TU{ro75?ri|)+G0z!Ib-!rY-giK6j(rC7<@m+`t4h5P zt~OnWR9i3PP^&M-5R$)1gWIN@8#!*ySq7Vp7cASiDm`L%Bq-)0cR@UvvMx0 z%6BnX*)E2usEfH(v5RF$Js&R7ai*3(PI4svEZYmj<>y#9E}CPJxMCa&>aG^uwUF*w zG_FFPmTG>?0QIk!+sf}u7WMh%!obqb%Q@7}%P*?^?7#7&WC@oo+Y__T;Fz3!C&zu+ z%V00sUR_H<8<$=D@_|$IX}Yi7**9GXRz)u6P}MHyQEe_um^!#N)O~7dKIQ&b!~%{B zBR=4`f?bXhLm6^gDzYrc(vfDE2HAsEWS($UEl&W%E~uWzzMw`P$AZW_*%rK(r^{V=koLjrle8Ud+SPW4IlQIi7kl=2YsLn6s&SV)mx)i`k!gIOa&|M#65xeOt`- z)EzN9Q@@Pajoqfy%`sb2x5j*tx)!&!G3!#-$9$H$A?EYcl`*SQSI4BLu8H|HHIB50 z6MhkX7R9(zKa6>hx+3P2)ZsBBQb)zSnK~xst<*la^@-`58Xq$#bx6!m!gNdR5z{lZ zS4{8JR)p<O zIV@C_qCE;l6rRCfTnalk^+MRisWE65^y8(er$T9ouq>(#`%i0!HNgkYN5UN^-W>q^wS)v`PscoU#r|b#cJLN#=L43~)nKgM&NZMrL2(3Fs z;y5s+T1c>J9TKA25b}!<_8NyAn7lXSAb#qEG|-QA^<%@ytpZ+DtpWno>p?;4i=c3I znmw_5gVH9&s3HM{#%ro*P_aodL1iYD3o4J@-k^h%YFV?X76GeN%K(41BOm~Fjz1P~ zeEi9P?c<9C6c}GOpy2rW0foo&g$h;J)>!qj6;=an+0y#( zJcVs@JVk7CJwk6K2@+4cgc;;HuJj<<*JyQG8 zu^r9wud&=OajIENVvaOouRa#36NR%)1=C$cJs>aT*KihH1{=N_WExQD5D_h>cS-A!e2_fT2gebgLx zKUKlqSOxMjqOQBS>dfQif$sKdmb-=uaMx7f?gpy7yPFaQr1P^Nm(ELeaiCaRVgc?SEqa$eLH1B^rV#J=xZr6qpzo=M1PX9EP7?i7{VkF zCLua0B{6zj%J}Gs*tt^PioTLEHu`GHkmzA4L!*bMjEEkUGBWzjl+n@7ln~OYMYuS^ z#YJ~Z=@C6BrDyculv>esQtC$6OKB8606(EA1)>Y46pAjK@_MvA#SvXIWmf)hbs~QN z99m+H_Dczj4#IBLk|X)QT5>G^;U%xDfhkp0*OY21K4nJ!e=V7rf5DO>)?lvjs;gJ5 zEmbM2Mb%Zsshtw;oT|IZt@^2OHH6w8qT0BIsxGc!s=KS7s^sdgUUv;twO!rSH%|Vt zuDhoyme5-j;Ddj~sJC$b7E%DTcC}EQT`g5NR|8ei)lgM&HCDA;4z<@=Q$^5fMHA|% zXjffT)E%U_>I8pRPnFBnSCw|ftM6RH)lipR`MGMTm-v!?DOVG9+to@9afPTJq%n{* z2C4?M?O=MrNZK_!`N-w&ud33n0jja8Pn!;+SB#)Nvyt9QwCC&Y0z51$U=3FVxsxbl z9ia+at*V83DWRpxoiJ48N*Jc{BorfsvT7(b@FpoOu>`<%_iD>McdF$__nYcxdeeLE z#g=RCp{hGM7(fngx<{yY+#}T_av;60l6!!1&;#qZ>j@zr5?l`rCUh3~|t=@LGQ4`(m)C2sa(L>w2Tc~c7s6QpTL4OZc_0$1s zyDX)u=x(HHQ0m%lyIM>S_M^n1l=yRZT~*L+QPdha9E!%17}Bt~vs*(|4(ls&SE`z* z#nkI+>U;@xp6Uu?rw5C`8Hd}fv7Q~)8;r%KjK!`V>=}bmjL{g+eB9?-D|uCgVxQZFQM&zVq51td^^3I1w0uEW(}R{_7Dk{o{b zlbWce)WjfaqM558eV`=qzQA7~4TiT}7t~qTV|C88!SaJ^rgj%!$6P~HJ4QwvBcmsE zcY?kftoo`Wt~b;@M#{I0lxmEWx-MoN`gR^yZ}knM#Fo&QS*j&|TdE$Uc-+-qjdjJT zLyV{}MoAs|`cYSN)s_*|)l~<7b=234lzim-Fr(=fqp2!m<10o}sEgT(F_N2h>#0~g z!kjilMN$(v6NdSXNE+@pGO4HXr#6BT;#C+m5=f1NQX>)6NDgYtLaiuj#gEtn69Q?q zAnQWz|IGS{ca2o@==JmIUAvvF)If52)5)JpP{*t_RRzCj)xeMKU4DUZF6pe_`J`KZ zx0Am0`!1=0-=m~Pe%~jx_G^<=8TZP5H~AFH^ZPoffZx5OoPGf+r=KP11-}bPFZx|dI%YkQ z7~q#BDZ=kqQoM>*KU02hzx+7y1M7;!jn)H+yR8Qke^yQ0Z&*gV`&q^?LIc%EHO@Us zO{GkKI!~&`jL=`4!_-{H$YW<4;^6)kZa90Y@0@*95k~7>?33JW)f7hLC}yJoM&Ds) z6LpIExZt#_FP*j2L1$f6*!`l~sq!h!V$K_vnK$k*Z+!38ET6iqmbJ_qm)zHg;hI|R zzO8n-AE=Y=oobo;fZFLkp-wP9%|3mY9DPGBf5q;7=8nzeWIywP^yx#)17DMyUucyS z#`-4agne$Sb*jZ`9nW3SL7vi7<`$|H{kSG2E~vU%a;R?kLZOeld%nJI3)k<*xN_g` ze~#k~|E`u^?l?FEIp~U3DnqR@-m${%{(i#WB!X{{_BTb zEo!lJ!XVYg-J7tzRet(;0p>tE^Iiyff6>)T*~oi7{{A}WHvGh?A_?7;)!ki%xZ0|B zDa$m@z+>q10nErVW!^ zRyaiqt)O*wGpmoK)Q@PPkI5l#%kk4!6=PKY=!{oCJBO>ju6F7nEp(m!vemezI7;6* zN#E$>YOUTPSG(vRTWF!8t`hwH`ijhrxfu=Ns#3lHc-7rLUq^R`e4X5t{jM`Q?j}{t zSJ}PBx`z4jDD&o3&r5#S7y&;d716>~vNZrUGB2KGzWmlxM7!!LrNtza*2*OOqDmw@ zR;OIIEq7esS-y7_)~>kXImeu&-fmE@H`Ha<*Xp|KXZ1?LNmVl8lq#NZL7l`;Bj&Gu zEW0R|@TI!s+M}*9FNHIs$hB5>=B@nny%N~HL21j%tl?tTaMe=Tn4h93S#jp9hAx}> zz*<-p@XJQ&WW2vhX*wq@&i9#nNxlv40)F9)>7$JM(~SF_jN3pJ;dg{_eTp%?gR!0~ z=>zL6&Jd}T{0Jq#;_0ojsiImS~U|*x7VGP?~MDad}nFz zk*?7y*}XX5C**r2Jv&gvs|U`ZsuOAb?i{IxQzI|a!wZpzADnNf3iR4+oWTQO27Te0 zv%3l+54W8yR7di3p7~=KH8+A@8mKrclb=J*`l>wnKEeF4)9Fxw_v|T3&teq4Kh#kKcEuDQE$&E1{5l{Y+fZS_6%Yz?^HZq7A%3$DpK za4jCpwRkzM#b4uEyox8*R>NbrIk@JI=9;?z*X6~zE-%4#d0Bc_Zn-8`k)#o6%kFu} zmfMrdmWR8IV6M|cJQ22R_z9!knlg`fN>VnBYjrE_6RdVyM{`zxi|h1bT*qJYK1LYow2*mSx==o8>@%3*D=n0!&G+5EcKdSeO37{_ecG?KN>(A9iojc z(H5;}i%zsv58A3H^&hD0wpz4TL)xnm?bVX@d7bvDO8eBL-s$W3DQYW8+q^>Cl&2jl zV4s)vc$u~nUF04rl(q?@ZE|=bZ8<%!`E{gS{Ad?{>a`&4l8y6oARJH3?q^NP z?H8I<&aXw%ZtGWx`>lr(4_Id;9<-(=uE)=M>&nCp)>WJ@S0`?>rg83^mblzHJ#mfo zlf>0*x|)rj+181PbFGsS=Ud-R^jN20e~b3{mi8H+*oS$wk7bm*ujNh7Zs%yf^R&~8 zv{Q51sRd_=y6OyPpu3Eb$MpZl>I`kQiaB>RZTf=yt}0C%9(Dh$8o3{;KV3KRb5s4! zO!bHB8x=$wZ*!kfPq+d*?YgEuaPMXw_RiH`xDTsRwDA%5N!1Yh-LFVynY9Rgws+#nL`_4hC6<6x57}MW7`>8Tq z$-m|rpsLds%0C`depj^|PyuN}!)ybhcM-awcNM27a2VmYk(pG8eRG%%5{M zQq?#gIOsFS8P|cTj(Whj|H|1w#cp#T*egDJ$Sp#YYbPjL_ zTnfmSrFE96Sq@|g3alGAA#hKie^AY!#GqY4mf-5a?*wlPeiB?Q>aeReNOMw?Zf%wJgVSU|FZry{G0oCqjeJeXZokmMr-|d`5&jHF8km0|C9Ez1(Xh`POG&H=n*iCwi_7m zcEB`Ra8t+emCEVr}#Ml0tE zEEf1GZQVAoPv9t8d~V=Jfg5P|Q-NOx{zB{L3@Q{gLQ3%_sc}f>kb(4^X(8^A zRrH@jA(ujapcjROz8v~WXw}e0p`Ahp(4(e?E~4jbq3>Lv?>r6-4a*x=GOS8igRl-^ z{pe|v!xn_CpuZgqJ0JEPy)Gm?Pk4#&D&Y;nJB0V62Tlr~AHF>N{}6W`@KIFT@8#JaJ{y+!?D!P1cd>U71r-GmqatFY z*hR7X|DJnib~hm)zW4ck{AIIr$e;zh3JV-AQ$K{!=b>eY)32 zdlvaO^B1JGe44+Sy1!t0pqO_}!9n3VN_n*w>{PIC!GN423QprJn2R-KTEVT9ce-5D z15Z%P3%pkFe!(hg`GH?Fr4arr3UUg|c}))t;VrM`!ZC%r6%NQ5kaI}k@rCnz$>6z# zWx210gmz`&^uk4X4-`IK_!{uM!W)8u?I>KOWq&CQ6_pkVZa`6ek;I~{Xk5`jMaLAK zS|kxama!wRsYO=gVEUab$d}Z{7BKuMUU`$xo9Eh&Y~amD_GpC*ePx( zP8IK3d~oq`#pf1JE54=pf#T&KFrynE)%V7s^O?p_ZgC9kI` z=eYImTp8GC`mtn@w)*K>9-7RTJ|3^W@8$2E-E+mgt9uK6SZ|YGExmW9c0ljL!saEt zKc)AXy)W&3Q}6qFKij)3w=B23@Xg*C?>n;U8LX*&4u5&!uf3zC6{Q=MS~`Qp&gP|= zQ8=dbH^1OA)V#o+rGgP12U069oKm_Etvb8(pE{OOFE2Fp?iPE3P0RlKv!`=<;D~e! z{u^xtx<%!M2k6!fDEc!Um&12UX<4rH*8`=`m;RyVYe|t?TKaA2Q$Al=QQ2?q$gD0) zlx-af`5DkJ;VrKl%kM3JsQkt9cgjC0f4tx@e$QdN z`mWrrF!?2C;VP~$GoF#z@blHV%%K&+Ul^#X_}^Zw7604o&%Dmp@$k5=D3GzSOU3>b zlPgZDxKOvjqg`EbYsLLtFdk*?@}UZu@6GE0E%Rc9Xnv<+o^KiN)fK9;pfXn^<$(7RsNAu1LEG<8)^0LaCDi01%4}2WGuQH!CMl^borW_f1wo>NC ztMX+AzOLlW%J(Wq_xh~TjQH}xHM%B{_mZ43vR6bxeRBFpy_j>gt_|bdqH8<%IiQco z$UM}-b0#K3ggZYlAV;uM`ixg6Yu=0Wd3xYqeH!cr`-VPu^?BUINM>xv>j37jCkigY z;+#IRp1q-Mi~WzZrWKrja&;&;ZMh`$zJ2zFeg zGyZ+tUuD`+QgvXsrb=`qf^}5`a$2ioSGsf60aa6~&aB#~3*5%>O%F^49Jb%^sHVE6CU$ZDj$k*+ z8&;F7@nnu9qoxOTs@WgjqiUuHPOiC#n&7Ub^tgH|rw3-%JYKUa@-Nqn>Ax_ou`KtB z$kdX~n(u4u0ix%xy-enN_RLIR&cy?2)~WC6bPPPnzglK{S?Ahm9k9!QA!PXf@*1i0 zT{NsKL;lRu>4DndUIW(M7sk73dJo_1u8}gcNH69G4t3jd!hrJ!Ts`1F1CsdjXBIvB z!^l3~gJmy&r7rZs+XrC)|ZyJnhMcjLzwSk)Z;{>ICP+bUb>J z=!)}peLa^KI6dPt*lXbUtg^gth9i6EWN@BuaJMUg4tLJV$n;eET<@|-c958Q9ycy+|U(*{l*c? zzJlx*NWTkq*}w<-%PZsGe^)ctsH+F+E7gIqL1lviCC~f4*8O=ocu?J-mO<<0+b(bC zLHiFfSI0-ecX~Ho$+UvN#e=TzhW9j158Mq!xaxvWt%Jwo6~2Fl);-$@y*4;^omylJ ze=z8ab<)MoZ-esirRP`f;K3!rBXdj#ebskxb9u(MOzpL}?1+XA-eRza6U^Abk+rhR zqc3B>y6@n_2A??ig27h}zG3j8;ljY(gS~P5R9cV6-@TOF7}Mq2wxjoy(y~H%l^1^A zd+y-1vuA5YhrA9iEie3(u_zk$FYT<9>(cJ8wWYH$i+j-S(N_*0-D`9g&pe7Lug@RD zKHBqreChT1f*+4hTlX5%tF!WF-S)0BYeVBpx5i3zh@q4XafZ|n*&iL@5+`7`eBbnZl^^UPdxbdUOm!Wlj5uVvQzu6$khH5+2}fFWhMtphTz z?lNZ9S=Sla|LN*;%8+x^t?ETS&-GgatTyW)>m2JQ>sf2D^|Mvs+uXO8?_}S#zFEHa zd_Vch?PhyV`y~EXW>(hgsjPZ7etq>1kon&3hGEFIRBOI>tTP-ub2EE`G52G z4QvtEH*i|u`oL3x4+6ghN`sBT-Ge6tuMR#Md^flz*gG`K9?m`QN$zV3@GSdseKwY_ zq5fE!?(uo$Y?m_2ULLZ-eZ$?o-6HI9uB+TPd|G%~n(Lu-P59pluLwsXLn31$6C)Q! z?ubkZ&5FDi`8r}pYoe{uNuhnBr$(=fJ{i3*(h>bNTACv=-97lLaAQt)iQT$k#&(0V z`S*Z!g7(Asa>_b<`uBQt7<~_=wX7?0e};eOf7H-(UTrvbbx!`D9I?{U(sJG7%r?tgNiG)LQ3JQrfGccZF}AM@d<)vOlq&A1f{As4T6h4D=~0sH~_g%gL$m zm6lfI1p4OWq1g~pNge+f~C^iYJp}Vp&ASN@1D)=+&-CMBUV6MykEF}Szyyq&y zd_z~y)+K51X?efD66gI&1#^;maOAOm+cy~iY~a2+Tp&}7f# zpY~?PyR?X@6m)~Tg5jRq`)Al-I2Z_rJyl_jU^wjMOGcOhlfCX|rLgcIk`9L?z`Bya zV^VPFIvwR&=SNDz^5(yr+yhYS68!``J5kHaS@=0B%h@V3_&w{cgl8t>zOH#_O7~9% z#Uje_gELW(nt&W4qa76>V?8Jx$bWAo7yO$v-63OKvuo8N+5PBh9BQWNrUf{HO@3(N zI@GSVx;MJCT-@Mr`$qlsm`kfY!_wjDkQ>E1j$%Ob2|3f~%76*D%So@f?a}-Oa79I4 zE0L4e&)Tp7CF0W6lsvZQwi*zGN|kMc^*gD5HD9@>BhbSqu|GFXW> zoh4SK?Ir+5XV6t79(QfGrZd=0>vJk4)6?_&2AVgX=cVWPE`{Xl@I z%)3dagm|BB&-#p~nbH-Kmbgi~rTsdFI-W3u-L_Hf+IAOD2N*S7GA^yTdk<#_CXZZM z*jx%t)(saYFsMDE?gk+%=OjZIW^-?$@q1^h-Hy_?gW0IAI1jcTrfhGoqRgsCFM8 zp)@NM@mp-Unsp1%?3xc?*~A>f?!7`38IB{zLz14N$=0# z^f`SJmNH>UA6h)?C*P0OT~b)?mab$MkX zp+@iu$SU0k3IF6q^)GM#Xe z>)~**o`fm|?GG^5x=oc?OzshSEvGDnlbx0Ho96~;kUaY_j9zDFP`Z|0y}fSEQWTJe zXSNmk5{5Y|`xTJUm^F!(`m?EWbH(UOHzw+BBuo#<4MlyVr&w+RR1T-Bl1TmOClt0raMz_`+%;bR4 zDy`GClhO#-aaRA8>dSvm-qldl?I`SaxcXG`yQi?u6V7so zV(tXdgsHqHhd0-Jmw9snyab5FTMHT2U`k)rbg`L}rt4hREi??q7u^Jn z5Fgw-hPtoy)T)u@q@D8CM23}VQ)YrUGq6g2@CuDDVuDPs3Z?Q13)AgSLfMEr>#kO< zQMkkjCe^g7(7`c@-!&VFEdgC^Mnc3zL?EW%j0-GRpu6Q&j!OO@YN|xcJG4HT!@UNv zXz+t{ufq2fw{i914iV2=Y?{Dj`JxeeJ@gdQI0TFgy=0aOxld-^RNPj|-BcY3A?SAL z#PIZp4x>vjDjfiBxblY!a>9Mq3|efaIrO0;`Q;p1(QaCi<*V)wv%QqyWo?&4fq1Gr zjMhls>Cn*R>^{l(W_BLpux3R>tIV8))g`-a(<`R1(XE%ij)z-ixT%>~cowt|Mi7tP zxM%v=)Kv0A(IeXh?;rr;y-1qINQ^Ub%o1#5(+<)1LMM@#U+1E#&efwkN$TmYq%JMh zQI{y=RhZ3(IG$V~q61t}=`?-7D~2?))CncKrfcTGJ66vE#tqGDs^$|MH!A5#!sXT4 zgxT;)^WfC!p8QsnR=a}Yl0I|1J2&A3ODP-H97dGG@HsjUe_%hOO^as zs1I4L18CK=wc(N5Qs&U@=@K_La*~=f0&873;b+kolx~}BAYHl)9;3~iO+3X=IO`6g zF|6kzZ}H~R+^f1J4S!1-ZWdj=@|xw>ZTJg9wPheB-)amYQ=;R~Ae-#wS|#Y4#_MI;ms`~7OFd0Fs{zwMsm5Rc3k3 zbukGj!6B#Lfv@#M&Kgpwe z#*lS2txo1E4jHYcBIGWKy69 zU`-0K<$YR2KN)Pk_+yRAyna_BtW&M2)@#-RORz;gLmTZo&-b$L7vCoKZuX^ovHnMP z+`lhhMDc|G8~>*K8}eO&Re_Cy6N4KCX9SlA2Zjy`-5OdJstMl}I3RpZ_=T`9(iGVt za!KTdNUrbwNO^RZ=Pm!UqKknbmzomZ%`t1n+Y5t4duMv5IK77spzJC=xY4K0a{R5i@Mh3PI z{GDDsEpRapOy9*%XuKZipof16aIcX*-aNQXa4&lMq~L|Y_B1>r_*igua1s6gV=x>l z3k?hn4~^oH>VrbZhRz91BO@LPy%>6jyjUIbhfBgW;ks~Zco%ZS=szGlg>M7AoP3!U zzBT+1S@U-Ilkj)sPEn*fGK>rw9oZu?fjrtTGC6Vv*>q#%-pDiL)IEIF{5&$NuWzVt z3-W7KRT>NXl!jTN-F9ja$Fu`lug7H_;<5!?Ozg7vq`t)|BYU5i3V*7jn*iT?FT?-P%#5 zzeXjSOX_3QBx@43wstk_k~5yKSutFvlD+p>_dzYwBmeZAJJGpiw9HolJk{m8mZyV- ze`NHIZe9p>LUdNn^F81*7Q~8y{_6SlocD9KSKF(NtW`OatY30+Vx_S@u}xwlV%x>m zsC{Dc677|~!(%7L&WjDTu8K)pi4A4;>!EINHqER54LN!3NW>2AA}xKt6MBX7$=Hk7 zHm{vD#*^>4d}jkJdy<~sb(J39hLxkcv_WLnrC0FFV&A3PW%%~&l3x<%jNOr1*CgxL ztk!L@o@hvHCt0!FNmga<#<>|k{+62=Nw2w4%}~9X*xeSXjocPUOZIlP9A@p%qugOx zR>s1yU5{Lzg$*ga*00TLD0_$3ru2l_KFgCm11IC>A6b#E@QJo9vv}rZ)kWV$NL-cM zD`<3Im+rNHqUBWUHr?(^BWqM^)QsD5J$fI|_!-*Lv;5#khlX!cmBZq;c#x%1NV;nK#cJ2&v_F& zL8O0m{oce@_7ZUa@*UYtPO@i>YKivpjj;ZvPR}=)cTv8qC-RaUyfVL3XW|+)Ghpcd z;VZHXeb2xH`9_CcPud}Qm( z)aT__`Ye~@L+WeP4sNdR8(5kpxp&}8*Tal#rnYBb$1b~q48^PcoIf+mD}%jI+lv$k z)VtuefTL?$MH_VKNoJR+9w@tJ;YLRfE;u#|7TJFkWZH3gH(IuCsZVwL-q5pFw{Fp8OLZyngMq!TP=RY0rR};lzj6d$6K{9X(*z<*_&V z*DhsF`Mh9ld{@k`aBRg|w-s}-?5<9=HoQgRQ)e{Bzs}NsZr*K5UV0XWfbZ$$(I;8! zUV|1WSzqP-mVV26DDS!Mdw5}+eu>woTH^}ke6KYla7SRv=%hc%@@$_Ditbl#8S{DK98At6>JScFmhJ*DkLr%*Nf)1wnX$L7^ZFz=gm#X)PCd!jD*USY zaQ9*##`pQw?bb8aN7f%!+_$A~Ki}!T>wFLR=KGfVe0DWIc{tuaE9-S`Ry`ZPzWRT- zFY&4E_pgs<{j?f?(m&pRmj7n0<2nDw{_p+yfz1MA1BVAL3d{)14t&bjo%W<79_$I5 zjoUQMk)*b38p`I`(E71mq^AY%;A>Vt1h<-ZNl3KRhqejr6FMdI@6dgrS3@iKYSjVZ z`ta7_L%9!fmVFEF|Lt{4`1$b1U0#1xU;62oa3~xJr6FD639WF1r^fWh6VWI?Vi*j{ zOhr-|XF-%hv!zdK!`Up|n33vwWA(2?#sfLap*@nOsN|{(T(`fMO32l$k`G)6 z_)!2(8f*O}gK+D=^k))(u_Cv_c}OOj>-XmdbGx)XcSDXIoi$43%x3{;DclfE>*H~) z1l+DcW1+6*=6Xrz^Q|TSCsw)SV~|nnh){RKOT1k&pwz7edgyze%k?KlUv&o=3wB94}u6zLyczLR7JA#D5ZE(1R|# z9LNMgL=DSKL23)Lq*f@;mFvT}FHW3u1%z^=SBnYVtB4OXghA_gS!&XvbEdhK%e=~? zQem=_!*6CW(E;z7S3;@K&C!}sDaNJFAqM^<9NNGWQVSU^OgQt>gk|a4jaP{wEuW=E z480^2t>zP{iJ!MzO(i#DOZqOkM3DVJniy!R7Z(==*S*)k70Ymmqi+jNTliXP5m8qt z-m2uxb`Xbo;-+-F!LuXC?0Q$`EJ(uQGHMmNv0k)v%*2@+uf<|qJE&>MWUU1T72$v6 zgv}Py$@ir?CgX(RA=$b}%FIN3!Rg*HOd8S=6Il9J8hy{dQFywrdUUUSg78`dvsX>C zy6w|)LKGI^678Z**9?;;8N4QFU4~1OlC!SE3vy08nl1GwWgg0W?igWlR(fiZu8M?m z@(N=4=Hp~RxQDyYdtg+gI7)gI_ck?o$j(gHwhdR-r``rOA5Uk_u!u5oOCK6EHH{{U zn&Q`NhDi$~FTR0BOco{%_9uETAtX66D~5UO&tT12G^H(=suxmYM6;PH8z9Z0QBqCv zn}|H7VS2+;<7wleHKEGFB_t!Hl8*~gHZreBC22H@j4)_f)VZskC>EsjvF>Nn**GZ7 z8cU8|c@v6qfQ%ndlXnQpo!xa}2=W<0ZfsPz3f%;9TkI8SW63ol^0_Z-b+XOM!}1_5 zq>rx4g@+DCgDRTI5<127JNrA`)UHywMKsIZ2AUnF-V!pyyZV&LD$W{FR8*MVv($C? zbQnx4jj!o*)LlO{LW1Q{+^T^yXm0?Bo5Jq7o7wCbOa}H8nngP5J+GK%NYiDZ1`w?o zobE!FxakD_2q8(Anl`zO(m25gRUgt<9+U98SS==XBTkti3R1(Gt_4)`(_1mRwdT_p z;W5G)P^gAo$}BfLR_%+ZPCon})D1M;U8FMdqQd+omkIY+!!2!a@vfVu30_Wl3F5)o z8(5JQ$M}h0o~XJFrG7~n{v}!Jt(j~=7+z74t(>}-VO`wRP1SZ7w(Ym0O2+T4QmN1A zG&R$R?BIw)MxAc0IGFrQWUsMg2^Few!f<6JgsV#@NT#{EmNk{?eOH~-%4MV{dfBY`+dIgZD;>w{x`X- z-=0Shw&(GK7>^uWz~cqs(AJ??@T}1E(9%%<@b2Lw!*_%q3%{?v4{yeIqrK)|9L?Pds(~-I!g}f$A9b587z(e^yFl9^}`=&H3%MZ5kjV@|1kxt)HZTfahGph$2;U=>eT%^M<87M0 zss6VF(Qz-Kxq(c){Q;=2yoOsR$M)YhH= zeqP|_%5g+M?7y)$6*vH)cBViI{Y;91@Y2WAk)fsngszEsTBdcKgxllh_%o zGLK<+v!m7B1kXX^o-&0_G-Fwy!@#QfjJ#`3iA?^4hIBX%aS zHRkejB|Ni)Xtr8d`37;c?= zlG;tzE>wpgc-P}__>QOctgf9$?RBXIFH=|M-U_rtpu7ijG3g%9gPr;W*xdX4rg_)V znBz$^LDJm|Vsq(R0sQBx`EXzKB=v~b_}3Y0g2*}ARd_UkO};58>r|CoCKY=?;0^w> z;VF@mTz`o-aXT4<}1{)Tsgn2PEn^aHZNDncZ3*B z_`k~i4S$viB&qnbuAq3@GhiZ9T!+pl9D=Z@EYTH4)9+HTqj{&0S{pDp3l zPwWxvQKdXT&2cTYd(u!g&xY3Lc9Z^qA7#r680k%QxqaXue%9tnKAq@U=WNF0Z7jHb=1GgHu7^iie~E&$nDI;Wx)#lI-#qkf+Qp4& z7PTv1#RK`^LYkADDbOL@u?C?PiMg~_TBDfAG{Mxk*C>qcfuL7SW28cygy2zGZ0BGt z{lAZ>VxAJzT9**6noN;)fL`EWcZlq@uOX}d@sI|(htv$jG%oZqK;H?Jt8I&@K~8%Uq}Fb{1nD&`qe6#9z~m(hVTieH*Jx%H!D;1hN=<8m^}Kfh4)a7!>4sN2 z!aK6-9&W8t0_!qr6}pY4ak>Q}ppw6cMadA4Z2aUOmli?bzsEgW1Vk|NiGw*90CG!5 zW&UT!C1iacZ@`QaVdL;j;FmsJv`g`jX@el((1&Kp07jdkYGp#w`gJMF#c#K8NrL6b zG)MzlDtZ25npPap4RXCS)>QIVA?g3P6xOt~G3csr8{|oX<*5O7Ln3Mhnbh*#25FPx zUh<#sgWg!T1{u>5ZNxZl7!5%)A@YofOwKe@+!R^YH?Nw}X~?FNn3kB+lwt>=CnejR z%6#CM14Y=(ZH6v$n9O3V6;F0xwy#WsEEh&BYrdn6(wwEjw2{1D!vgt2TIA~X$i5=IwvS-b$7 zn7fP~ljar<5!5uTL&I9$@FzPy#OT(V&*O4!8PEv38Z_WpfS%=nhPY(-NE&QJy*$e{ zuPGwNUvjZ9U+CUb6*paiu9*fH^G2U(NE(ynhj^T&nW~l+tgFV1XQRhZ9~)Ps9_6xS z-~mNyqdeGr?9o48f=7Xm)mi)pV)9o3u9v)$&mz@bEYos$@ZG@MjX!DfamOAr@#KTY zA3pKWBafJPP}Rt)!w)|4_M?iF+)wxyKZh;XhuiRuXTfdM<$q1vf@6YJY7jTP+JakC zYo^>r*G32HsHcMCk2&VxDaTHnd}P%j<0l?|@Ijkb9XR=*gZDdX^28&Ltr}5v%)y5r zvgw3_#~*a?luaj0Jaoc-#~eF;%CS`g2d;}UvTB#H$?dk=Z@V#L$Ba|A^G~Id@#87l zA{Tz?22h`H^oFz{`Rpe|>Hf&C|DTU>-HvJ9j$8la_+Qgt`k2B&VgQ`hL!H92+7PF>QeYjNsYow_!sF6GpXa_SRKeXUbJ%&D()>g%2Q z2B&_wQ{U*+H#zmqPJPm;Z*l5do%%MXKIPPpavBm&L#@*=%xS1|8tR>f2B%@T)6nQN zG&v2;PD9dZXmJ`^orX52A>}lTa)u|I;kC~2Vb1V6XL!9cyuleh+!@~J3~zFVH#@_V z&hQpzc&jtK%^99@hL3U@6Ha5T(>Tm&taBRcoyG>Iak$gi=rlGtjm=JD(rIjQ8e5&l zHm5P=G>&qb5>8XC(=^O!s&ks^ou&q-X}HtW=rlDsP0dbI(rIdOnp&NvHm51&G>vka z6Harj(>%;+u5+5}o#qCodAQTu=rlJu&CO18(rIpSnp>UbHm5n|G>>wU2`5?WB!@Z4 zIwx80BpaOMa3|U5B%7RMvy)6Z$rdNs>LlBoWXeg7a#|8jORdu~%xS4}TI!va2B&4X z)6(d)G&wEJPD|2hX>nRwot8GICFQh?a#|BkYpv5d%xSH2TI-$G2B&qn)7t2?HaV@$ zPHWO>ZE;##oz^y|HRZI9a@rD3TdmVJ%xSB0+UlLQ2B&Sf)7I#;H92j~PFvDxYjN6I zowhcoE#TdGQbmSe%?gQFVk zc;0>`--Rmv$RX!kbXfRfgO#I9?V-!4D%YBg^&%}7Qz0!=K^a3TfJ}c%!((ZS;-w$& z0%(>+)5gnzN^Y?7mA#z#4jf%K?NnH{RA1h5NK3>U1BoVCO9aa|M|0-i)zQa_`sG)$ zqH_J|+3xMuL{g&SX#Obj;+xT@Kcmuhf#ofJO3kdq7LqyI-w;?ia#cR@Gd}sLiznho zIWmF|Q<_M!TLv~C*zq|CHN2em4PM@3*DVsC`R{MeMsEPD z^puZCi%)z)r_T~WiHq13J;r}&qp?Ya!+iUM!S)80e31uJJa47tVozkmD>=)XJ0G*Z z#piS7%3krD2YgZNOMC<)ewfxtOC?_7k5EL1$sg%GiJ$HI$+I0UUE(Qm5?-;*cZ;~% zOm^s-@KD7osdM*v;dgk`-%~Ee6v~ScE60G~jT6Z;SHoBt!Sp0qJ}tFY8b zz(j{d=z{1m=7hxyrwOh&;5nLHDxw!!qB@guUJ!%eq*G0LNyCkt@bZ6AVoqZOYar3# z*Qsq9YWUqx2ed8;nQ%#3n1Bh!3m)CH`g=V3!pd_~l1l>k3C_WcqH8< zewk>Bl#iq{K@vT}Wo!zKtu&lw_!%4A?SpQTN7`=ESt2S4BvV46w&1ne*fU8W?a$Gc z#k%P6l0$TRZG^-jKKooR#U|;nat*C+w_*$)bI3KE3_~v$ya-DQnIS3d70qJVB&PJf zv{|f~*hp(l3L4nN-eiVoG?6kfmdHi52GJ`N6L%A{7!4*?CL_eI;U}Z!{EX)- z{^t25QeL(i9*MEMJqh6uZV#8q4wE&WMI$AaBwI{-L~0xtC<6&W#Ga^L?MF21gH#&mryEYkB{HXHB-n=s)0CPV^30b(Ea=}-Fx6IdrW*RWs5z>ewWg3@2U<%&$Ws7 z6CY^#BjfjgueDoL5V^43&)ut-Z7QJ7voLA-)E_CVNWtvDH$J=J%UL|1W%*)dl$7oaLn#d@4>I^9n^e^kUd##2HhfVtHDQXe$ia(565HZxA-Nyd+AckuJiAL z|0%mFeku5neSoszOWAh(AHZSaeGYKQ-bV_-gEN>TvKj`7;>UXW!iKZs4%p;NJLtn@~LM1i-@#-|nLH03fKji$xy$U!;*$+5n9}&MBew%*z9`Rc8t^sR*)IEuYbA-5++C8p8-hRV$={{}q@{YdnVhz#ZnQ`kt^uiE>g zKf$=EM9#MNR6kNC?3AjYthEnP`IKS$eGdK7p13}71Nw68R{wXjYh=#WIomSr<>Oy2 z?W?hmR9kEQiRvrt4@GY|FnM4dL}}S&@jlqI>_|M9GGz~nUrp&_{9UH$d-B<{*xgQT z4xF+Nir=c~v--WF;Q{e!z{)PRdI9ItZxQU)+Q+KNT7GnVJLt9OyHfKHiMIn=_B-lL zV88vM`jGZp_SN=IW>t^VF`(?G1GR%0NiCl{3 zSypSkN2o2~kJz^LAf*rf)wmaXu*J#ly99^&n@AM5wB*1xNIANDtL-{YU`#jpR^E08bYgUpGP zgNVa{*d56DF2nyE`uiy0YZH?bN26yD`O^rTV4gS*eHMQ14cuZU;=_>-N&W#Zh`k(p zMHWc3P6BVyzI)*>M6MLQBO-}NE&0=)7@s%*y*2oMq1N}k8cV6{UsVTs@)-|d_Lt2;vS2KWzl8;+ZwkO6Wwnv|ho*~d{ zi0jwjeXJkv;GX=_;XS}%du#rcPTD=A`flCsm+X_#Uu<7yhbe~=|6=$o`wg`zWxn(m z?Fce&J_=kzzkN^HWMA6<+Wr!ECw?Vze(Y?A-5}-ml89JJ#*Bv;G67e z{qN~7{@-ukM|)b>R~$;&WRIv$5~udW{KVVnOWBj+cY+Tw4iehGq`C?D2>LrIbFg>?L$dk@Ne=;Jso)^@*4AaLuIHkOgs{d>!HxEWxSk5JKGbp z6R%P>*$-B)sun-)vsP$(C#ml!L+ta;qa2LfX82vhe(!SN7XCH26Z;Y3H&4Su;+yGq z-eh$Er|ga5*J}U9TDt%@*>Cor+h6qTs-~c))}ElQ0w1G{y#(jI@xWw`J>$msrx39KySbQe*LF~^CK;CA4dI)?m@}t4G5dVCw zr-0{AC5~gQouK=P<5$2<_T~NmjbBan`04|&>$ksGL-D7W{!AbjX8!&`_Y>U7{5`sH;>zrCq1fxU6T1;J-oSBsgKmiV3cT14S+=r;a61;3wu zc?Cl7h=bOQoVBZ2BNPbjcKg4*QgdU6WG!MT@ znE#rgwb;1%=HPv~!fdrSEJ_8{NKz)g0zIzoGb ztncO6i^;kTK8l{nl;haX)DZU|@Daeuew3aUKM#rDrpt-(J19f;-qykR)xv&gQ;k0= z{(vs?XpdnlV63<+Y`?uo~4~(dj-!n3;iF}=R#NZYxYw3E!MZE zwZ1*oF!xL0asLhN8%my+X}Fi)0v=5Mu0%d;zsJ+vQU?7Y zN*lZH!WSg(-b7!BJd|G{ZXqv^)OMQupTNgHBHjwD?BC-bP+I7F8*mW&ph7J_#MhV7 z;=1KzHEhI0&Y(nk~kE}|?*@aeOZPE%fKhlp*@1H+YLY9ijCc zmpB1E7WNa`zHQx4Y1>QHos>TM{dv@c(YJ~+WbdIa0v}_%9E6@?)~OxQQ-WWGlns?5 zE4R}7f`WL7);}e2Ec#Q7yK8`(>=UX_A}&q#$<=34`q(G;K~EUJUPJ!?S-;SqvNwre z2mc`a8V=pZ{%am|AM>FD9Aba82Yl?q)W_gs@GS=)fp50p4j zZN}}3TJC=PDEP(&=Lg?rK59=aNG#Ot-YkA2ctw1sP!3?c9Rq)mec(d!Bt~2-@zcln zIhHbL-_-9B=qWoCKVRFOq0Xg0efWC<^r4K~ZK20lH|GPV7_Z;K=VQMU2Dah*33?6l z@DY>|`tc4*#cDfD>zkx%fmxTUKOz1l_LYoiY1feWP|CqFZnfS=;*SFR*&l7<6TcGv zx9~U2`n?14KK#5(>zxok3_e91=V(1C^(vsB`D1-Kjo-&V5UJE^!QVoqaBHCCoRq@YT>Cp8`kNAOE2BUGDn| z{vduGuH`z_ZP=!*M@yMT`{-+Tc)l<~O@ zWrX&>q3LD*Ts=SBQGE~bPchHUfWM(~MCBIbn?h>~aLUe!|C2I^-%BWmGH(0SCFB%0r)JzoNpxG3LM1@h2qLXOxBXXAplC_P?cQPdt%$lDLFfPnRLr zp4cg|GjI*pr;W&$knjDm7bQ;D0N2ouuTk2Jvrn}AH;JF%PqFSl2;a!uQMud5=Yz!a zc;F`co&NLs3;lrjExr zZ}8)Sqxlax(RXL!F7(xK-L({a*k0scjC?Ko)@i^o`tvjNEB4(#;{PDpIm9RYW7Hhz zirKdqd<*%vJ7o>+YXNUD?&~%G;e4I6^h+RqF0kS{{R3^Mx4#2BA;wV-@KCN3Mo^a6 z&)J{C=eJi{pKH6}cof)&-Ia(d;`lhQ#r^YL;ZNDV_}Rb_t`8mnwi#CwwY^w;s^)u* zFG7}f9jmT}u87BM@J;q<)gKe@8uIoE@P*`QKJ6Mzxk}ruidVy@n58NyZT8=%!WZK{ z+zRMnd%1t5j`JP%&(K@Azw;br&>rr89=yf<`FG*-ZQ=RAEw4ZA4Hu0hMYzJozl8zr}th zrS0vgb^{+{eviRd!}ZO9!0m}$6T6Ww?cA3Bw~kIg~kK1n|a1D7nm(r$xUO+6x zyz?Y-gMs~;Z-T0UUQ7GNP|o45{s+20_fS)T8As|llY0!66`+@tr%25Xi0}s~_ zG`X>-`tX*=LVVN)sQ%PTRS2AXW*bu_)IuttMgr3tJjVRU9Ddmp4-AiAmn;aH8sn>S z<&dWuWno$sq@&J__BFSb@Ym$eljK(1N5YdyzapXZK@my?5r08NGIilzRU>?sAnGk(#o_R2>Gf#?$e}1AM-W$Ad z@~cQcKaPK6#Go7!Y&lG~%2{3#T5-fY+8~aI({c#SJUAc@_r;au{3lQtG1(&w{BI`+ zJyN7OCXAAbA}7rjP11=H7(*AW(&5tG@_>&}%tI*>RMAkNIgJs~lbrV?Mi%(I^b2W? zhFgMRQeX5-8X1g0mQDi+kqixEJ<~F;v&4cte`hFue3nNvOr+#&q9#cy-We^@odN2G zOJq!>Ou~q5(Iz^@iqy?>cETmC&gX3gj&!*kM!T-2t;<6#UfV>6u_o=62YQUhf)hVv zFi7f%hlWpVna5(XAEWVxzOg8plJ`7Abhi~IQEP-nxiKk@nMRvuQ#`Z67tq}(91MK91|i>YL(R6gZ1OJl->+9)z3SPUE3jCN_hQRPV(S-}a-xFNC` zhb3P-x3N{{n-!{aKJN?PvO13~wjyJrs&n`xtMj$Ks`H9~>R95lIu9wfItTu)IF~WbS6WpdM8LDGZ9~By2WG&e= zp*pu-VRe4aeDrLg)wvP+mSZasQk_fC(+hYfWKN~-t~TDdPv=se=Ig{@)_5Ss;6s^`)w#jW)awerH>q{2D&KU71ifQps`E&m zhFyuyJM*m0uR`jqqnX(z5a)%w>r zW7>0#_*J169r>0OIb*2mSYD+fZ{YLC#G|*Z<_v-_MVv>kuugt8PpzDbk7IcvdU+oe znN?s#=Hu^G=$QGImG=I-~3oce*h->*{rcOu>&!}~Gi0CMad`fwTk>>N>F^Q=_ocKKH47~XqApG@3V zVegwwROh?oNqtBy*^D+HR%}_H5}RMpGlBNB(T1`mR)n7e4z={PI={rHEg7B@-?Gl0 zLaer8FkYKbXP-j-ynMBC6*k|mpkMP;WK@CGQAR8V(}qocSId8(zeYqskF!qyd5=WKjU#;wll z(f=VazL@?)>GMtS>2JAe(ZTf7Y2?hK_|uNgZ;8=L>4zNhAN^y||4UyrX&Ze0l79cH z)9M%l?p@kCAD!=pRj((JtN+;Q{0uvXk`sIJY#9%XW?UlYf8up9oyb|7t z$bZe8^9k~wOP}xv^JC0c-x8m#$nP&dwK@mU?iZ1NocNE(RUQ99?i+NzfuD*TIElE9 zC)QgNi_T6fQqNo!1V1p>vX&Bu*RgvEc{!6j{%V;OX{O&!wylm@^sk0C2_Lqz)$y}v z*U$8AJHE`Nd<6ad(DO3o65yAJ!5P#$h}#$V{5E`pq0c6UZ-v$J&zD&pBhYymbKyte zz9KH2^jH~Bj?^EmV?4-Q6+gZADN-_nT_5Y+H^Z@ zT|r!?FrWOvywFg@I3}*24pmkYe(y^@4}kY~#>$paD>9cH--VdBkf-Co&5f#Mca*7R zYp}OFIZ%fGcae{;Qu>I~2c^V<`eNSu(ykM4V=lmlA?RLMq56JHyEaLvj^B{^nw)8( zJu9FuAg;qcwoaZ+%tq7ZAp8z}I~jS4`px*gigoJ@##IUarQlgILxrC0Wp(~cd!EG~ zRcI~yv9F5W2X8NI^9)gEDSq5YT#l!|2cxfwcLTD1bbLxo_k`zrWd9hd&OVhm=99a3 z(ytSV&zsAwrAIKwjx4tpHA9Onu@=sQ_k707bF4MLVK=(O3f05ElKMI1%Lmx{8vUOD z%T{7BZTo^aZS=beZC++|oQdqg*vwsGh1;3);$hY28^+jhaMuS^n?P~X~%>3@=1j{c@D7~NFNVC*PYP%Gv=?wpBQj8 z?JinkRc?#kgV0-2sv^%2yFbXoQ)y#6^U*-+AN5fy_JO}2aszU$$d}x=4nyZy9)kaRvei+8{2Fxaimg*_Qyq2CHZElhSE=O{ z(7qyu$KIwoN3F0r2I1Eh#B1|%@@I+F@f)-;><@M!XKxIs%CFJ$2KIME?r_GzkJOL9 zO&!`y?k^;^XW++I#QHsAvkH5A^KM~2zlpK+3H1f=f7xku?!kV488N$Wrn25c+ZFvkLH|4W`K*7xpfeBrZs>R)AHGD-2{v;vGQUzE2mW))IlNaR_bz@9 zf$uW<=4fK|hF|2E`BF?zSBtY>_F&ph`P^UxRA_?G!_REZV&fj&H&HEe(U z*b(vbS;X(}tcypY?=9jp93LiC^3*@P4>C`_ zNP8OaV+G}H;5x#p@KMSK@cV9TeYw;MtthfOo<+|$=z9qKGcy_2#PS%{^#=5wb{p#( zJ~yJLH}?D3YI!g4pVFR1*!*FrDt!i>mH7D?>(U|UeF@uhbJVhXz@LD89qZJ_cc_lN z;3?uJ&yx{}o7MRu?I>k__^wg~znN@xp39A&$C)ceq5pSm)>1BjCl9+0`B+!TyaW6s z{(gwA4(t~&mi+L&3+@JN9Y^ecK=w=anyV?BxLkRMn1$iH0N1 z-^VkEIrW|!1L(%oc6p2|JL}u6#k=_2Y+X-F2>L8k^39| zd|S!9iTscF`W@w4@D)RwL)_+7sQiZm>g<#05{&xdgq|(IGi!F5xTa)ZVmI+2>3r^Ke8=-`{@kTSv!eqnuwaTE#v-k?0rXU zZe@)3#s7b>$2g1rIG6YL*!y=_4Jbs<5^{ZGTNS;6ot?1z5%cUfhgluB02k86#k^;* zH~1949wS#9(fPM>=4#rw*?6n7ls-6#HoSnn<+Sw#>ZjhOI!|IAc#c^8X0b=b{@3ik z+bY$E&B&I~?t9UHKKuI4Dm89b=Ehd$qyFgnWCqs?%(L?uAHU>T9lP0T(NXyPCHfcB z_HEJiG4cNxJ2vrr3LDQYQWo zp%dKM@GXMx5ae&f?}gOgq^$+OFJZHQIr0B-brxV!99`R zUHLAu3%li?21G8wdT^iZ>G-(MygJC5d@-3X?jDX)zr+m)DH1o}_B{IfP>+1f2pg@D`HIOgBNcSC#!;O`G)UT5>AsKOXrCzfu^sZ(GN z=D}TfkCXdOrA9U%<9?TO=5m3)j^!Eq&hWb9yJu|9ACP}eJ$|E3gE=mpjqFSC6Y;d? z{H`Isw{VVL;b}m3=DR-pz@0DFLwV!UW>A+%^6*m#T@K}$WBwnX=yFCaE?7t4|8fx` z@!i@qeO#25l(g*XQwlC+N9HMZ5xKl*cb2NJl3ym+*AA2|T}ln`{~S-fW#Gt)Y!+uD zitmm`>w1`OF^k9L} znTzm3(AQ-lUB-!tUJ#e>6}8=`Y1cT4aDiPHelPue zU|dDKC00|bqijd?k8o5h$*=iSCDf``q(u@{S6~Jsxkg|fmJElWoISKGVIe|el(3V6~C4?mVYAYu1JL$jW9Gx z)VQd9FS23VQQo^d_xlW!ThL z#M9YQw5rH}_PVc>R#MJ8{NttqDI{GSS3%U=wyk_nV7rL^ys20utN-=a9Id5VH^j5yG4Glo+S_e!&>zdf zX1&36Z(Fi#5^F-~BQfo3E(kpcL4RISSgJLb43W{e>yI*WrvMbH`}?WnZ0QhM3Fziy z!G_Iv>lI*;HZJf@-afYWYO=_v{urqq%f?hyb>*ols}2^dWR$P{oy^ks`ZpiRXyLGR zRL5pbBin~%Ngu5es9D0awEuTr)cO<(15Bdt=JT^nw@Ii|1B+Q!rDqmxfGtzoxpcCi z`|s^zJB+;Atkagu+y3S)I~o{a1pfeO*qsMeOtz$N@yeGiy`n894V77y6%DTG3pKz( zeUzgOzMGI?cmL81=CR2yw85nttAj>G9_7vL8Cp1Om6Q#$pkB);$Un_By%d+5SnAWq z{*TIZl;yA+A8{6_GKIa?CV$?LFU!zmO-Ey6xtK+HHGeiERbNGF6;Krvk#-QvK)z)) z?Di+z^mMD2j$B{eXkwb!SHA79l8Nl!r|eIm_P16A^fg!;YDrpo%(B*3`@>_{tTUO=?$q#qA2jravW{Zcf)C(%;doTt=%m?AhPA9myYG zrDqextgA5gk+fp5;nI}RKj%j>ng}LsQ(6-hvb%jV+OkU8p|_!o0k&66FPrEFQKc9x+tBvq5rB)92p=3LB{ zdDyZqm@R52&q~UcbBUUwMWz2Wg-zLlFjop9?1_wgt4@)w)+Vg88bxPOSgMk+1wf9p zt;Hg2%YT?@Z)PN}k1g?L(h6AFc9 zAr=h#>_L(IwoE4=Ry75pkLH}V^^+mZFU!`2rz~XNG8fksNxG_o?LjvCw9TrpWjN0K zyNXPkfKcih5B;x?8P$+$OGhGC8|7m&$4Y72(s9AKHOg@qvuz`kI~)78@%W8@r*S9V z=F5C4Y^^zF%S|~;-L1*0uSKkWH*zYJmCJ1@ai6d7rwG2;{A9EJ#|XZo+>%_3pOvQM zmNngd6eXCGclz7VfBLh}w}?k*j0GEj4dL&E-x~Qc$p4LgUFbsKEbKI7fKPGD&qnO@ z4vFZ?iu^eG$MLJS5&X1IcOzb=U%%Gf!leZLO870S)kN$!!T!J4sgL}6^dd(O@aZmw zR$NA$fxq7Pdj-ED{By`Qx+Of z%{=ITN4yn~&w_j>urgQ-y}0N#0rP;m+oJ|?r-1%Ue^ZJXj33>lGnP7qfNRM^zwRD} z+!*BQqn87Fv5@}_KLv?1;pYgxp2X3U^*1fIcK(8Vdh|{qw-&h*+tOHGDVy@#+5)JHKK_cW=}pj)ll|LoO5aI{Y<- z&I?^0`zx?tk~k8Pyjr!!)`k4>Mos`@O3xB8tS9F4)w&*&d@2KmtlV-_E%wNALG!R+q8@GC$t&pw?#h* z{;pAvrO1~;K9szM;P(^!2-q!(ehT#UEA$_rpF*F49*@0I#HC-sCTBcyU@s!}qR{`G zTTcD~e?x8>@%}-)y6bfz`YVvH%KX&RR5IhQ{NFy`V}1&I3jKxX4@9pKdjAqfed-$( zY)d>pz#j!aGk!NvzrEO*$~Y{c{~!9((jP>>?#5_Ko~QAf0saTD2B^EwmM~v+H`zAy zbQk=TATDPS$8GAK9eM|sE1O{FTkIqN|00i4#Fd!15`b^vCqw@>^?De@*b{eU{IACT zZu%!77lPgs^mNzwQ1XqA-7dsi0ljL}=@b1ouyY)}G~`ziIxF-L;=Dnf^lS6z$iIMp z7QTK(UWPg)N4`IPo?x#7{?{`fYGOYxdQ;Im0Y3rx)WAd3PhO%#7z|>QbHl^~mMnTeua6h);Jd zrzVd$`kM;2mRaO$Hs0fuoL~eh#pc7#M>KwIrtfyvqrQ2^dtWI_<4lO5!6G! zexHKeBjPGSd=H5CTjJBxfm#s9ZRC28kA7`Giu#m8{v~z(1-Y!qg<}67?41Xf;pYeX zH(>7paW*B6%kY1MUx~OnLr;co2Ccj8`e2`*c>CtCALNH06@Cw}wCoVif7sVuCN~)` z-L=^l|7+og5NCYk-r#pX`78zxu)fwIKTV}k)J1o%jo|oEg8nr0>sR=P@wWlJ;n<&m zok!T$U6(nrdjUEd%GaTfvY+a1ru5WRcV}%SZryd#3cEq@R}jxb@@zre(XjsqQV%|9f>0e_0?Ts{ixSs^qJM*Ing|0w+{v3DF?1?ukcJH)BG7U~e+2<+U(PEYjCqZb{$m&i2& zbD{5p-w6J{=--Clg1ClLhu!GKhyN!c3yA+6_9l>LCHSkbr@I^)Fdz1zw-CMG(ff^j zba!(v^z~Gp$;7c2`w6M%8u-nb=QF^VU={5B!+xQ=b!TAz5A2-7&Nk#@As>_T27W2z z(_J75s9SvcKTwCU=-**o+yebQ^13^$2>lg_>qp`$ivC>U)?NLRsAGHNXEJ`r=nsYe zAN6eqzc2n9F+bzrrwe|%guRArN5a6td|wA{}#Cv;4Dy2ad^h~-D3V-1NAhh+{Dox{j=yVW;}W@ z9(u|}EBbXee_86Jr>u;U9ykQlQ#)RucLV*b;A7}f)Gs@BCSWHKaekss=g{B8cw|n) zdI~+9c*_!ROycQ5JP*(>gnl{V{hfHDQirzGAvyGVXx+6ko%*byE}8H<9J?*ZKPUWs z=%c;cpZ2{gGb} zcBXz+vA-6*-q2f_2SNCGL_NA=rxbS1QHLbxFT(z3`~@Q46#0V4B_SR?b>}^H)59MD zKNWg^!(U6jNAa(xX)K_>GWP2;?<+F#0+4sMz`p1Ii zmp#5N$W=w|0JsFy)3@?54=R(_YVwMK9KVF~g^B(q4u6f2&rN;wMZF*Mr7Cn<=vE-VZ1nA89k0ST&cp9O>|P;{T=>ylrn{-nC;ZnY zuh;ZnpkH^hwZYFb=3g1?b*29_@l8kXC2}#)>qg#FZNPY&XVfS&F)4rcuf zLvJB^kFYldzMh_!p7GILmYtw=H{C2yPX#K4J>B(}p6@4|KKpv%S5NP#MSp$#6vB`0 z4!h3yq$B?~jNd`*eIb6`wYLFzeO2!S9w+W-;6KD$k36%Z9~1pp(4B~X6!WV*b{?SL z6gzr~!V&zh$A1p|WhI{g^65+bmxzBp`h(Dag546>&4~OOD}cCusV5_#=qoORdnXy$z>{HDWi7VMA3 zzMeL=f$<(goC%4uDg3?ga}#d}@g@Y{A+NioM^k^@tsaMUWf*d|kjqAY1mva=$1~#4 zU6k#aM@8VrhOei|Y+_xw37*9NDfIY7s4oX}boQq-T>9FC-%s#s!{3J9So99y?{CIo zH1W2kdHz?-c^P(;KKcl}2{y_MHiKhwi>;mV4?ZEP& zo@N!5cp4y=13BFl*#m!-$g>W4P60o09&?f%@f~qxgARe81$jNX^(cN))v$(%}C&<31Wc74Z{@{zHxf73g0{zn(T! zka~m=e>3c*fj<=f1uz@-V#DtP{~~g`*q;ZHM-luzB)=l$*8+PbiRV51dGPi0nF3%r z_?h7A=@9FnV-jCS>=nT74D9>BMqrlItY^ek40;~)RPt^~-ivnle3W>jlkaIT8S}F&`ahuGnf@inSAze6_2d!s9psD9 zKZpJ!U@GeS2cb>ld|OX>xJg`di0eIZMZ^Ce__<5o`pMB4V*D$^j|{1#?K?{lt5ljb8SFg35a6^ap>;%{M3ij6WP{iX z^DuUPz)m*&?!#{@;yXy5lfkFp8T@5LFD>6=(=kuykbg(&9{|5Gaa;m(&_4^ii}2Ts z{(SVe0KWnMrA}?I%a?0kBjkd~YZd-8;%^K5sLhv>pDu>_X_^nBQUh?XR{CV=oj=%lrw}f8`eogGQ zq`p<4b3iv>o}FMkGU0zT_Ol|t2YEd`Xbw0V{yX?*vA+cSNy&E#_0wJV1&FUScG6=< zPcdo8xEDf>yP$lP&|8JxG5UX`zY1}_A9LT)8;{n#(V7^gAV8$unkAs++z zhVDt;xNbl`HZ0R4&4PmKTG_{WuR5A&-e@{5tr zLmWqmLr(#jz`VSQ{4V4xz<&zAEP3B0?=|=vjK6y5Z9{JY_Qw$4J^BZ-e^kT%I_xKA zym~WUosbVh{(J03<+#)fx-2xNd5r|?Kl~o-A6M|V17 zCUX0b%YxrcR^;!XSCjDx1MfmNTK5+ys3Yy${fTQ@8fW6+=!> z3kcynPfuT%N1h3YqX%{`GH+8sr$=uk{-3}vMf`foY&7Oue*7iG-wW)$z#c5$Hu72z ze+v8z$dANM9pc6Ube{P7d_+REuudU4`Cw^pZg@g3byZ9r`4B=?`Pu zpl3oC#O_S&#)W>bc}ZO|P?uQb^9%XtX}NWfkBFaU_!$gd2Tvlm0J&uNoyz)^0zb=; z>jWl4uRhmP?-O5o7MWi_|el*+hKn*{OR!Zbfap}dJ4x??DmCU5B?G2oK2jC!C2r#;(tv1 z{X?0*$OXW^k6eG^+Dj68D)LD9zf#}a%o{!3WDa(N*x#B%ujM@AZ}NSE{fp@7sVXs; z7dx;Uhw(^)2U`oruUULv9msMMLf? zmK__9qeM^58>`X=f1$YN6jGg-Mx1$%CdbWlh34I6pD*6Y}?*V-d zx(T=k+>Tysha zIY68fvHu6_z##go(qA3@myCy=3N({-<|A^okl%`4OyXJz9RYd+eskgXBz*4X^<4w= zg30LbhuuBI^O|^SLT`jlgxy)#-3c8R{awh%L;hQEKJ;DUJVczMu^S(|T-eykCye99|*d2`BX6!FH=ugRhypj1I zkc#z`{&D2@fO)b2%*OouhWJilHz#!nq7LO1KlQzb|HJtI4t@*x1vt;!2C$Fywk5nh`sCdfOcQ zeh>c@{J-I^V1Me(aVkCh7SPd$k%%24OE z)H4Ko1BjzCaeRiZ104gsSJY!9*b4m)*iD4pcE}Y)j~@{{;GD6UQ<3rfC^{Y%GuNeOq$W=hDJ#{&X-_+QPfW1D{HzR(x>27( z3x92xH`j?bANot_??wM;?A?RzioI&s>jnJ_^bzbuA>UHqBkaFMuM+&>=+#2+Hu49M zUx9o8b$o{XC+v3z=$}r%o|e{xx}`>c0s5QZkAzy)<$PLAAH|)lNevO}X=v7Dl zGIqB^-=Mz~;}sqHC4NG|K=_l$=M8mM%HhS)vDc*P-)X!LiYe?MPb z6B1u5>T{6zuOojJ`4H%q(1g!r7UD(!2lNA}cT?*7D}MGf4%^5(7INQ%uNd#r*nNWC z3@BzLuRrit34i&}kAl9QO127n&5+ATeuEVUSc(4B^drg>wE3GV`J^JB*u>Ep`|aq8 z2mgEcz2N7EK7sHW^bfOtwTE8;{uScLh5TIj%dy)PtOO>-{$KRZhh7OC3At9pTar9V zGXIC6-<$YmprRrE*1j-D>rlX^TQj?&~^iuFA<<5U1S?l|S|6P#-z7Y{q<7>|Nr zM&?s@{8YnFN9g4C9j={wMU`67M+jn}hs&^iqNY!TI!eL2npX6Kn*37yLiL>exvF_JP*Z zX;)L%j>K`Cd>Vi|z+(9M6+hLW*Fo#)%!A0IJM>xTZp2?5|9UF=F!)J`V-oW-26j7O zw>5SrkbfrRqDYSZ%8cW9__{6rDti6U%K?8k^=^jUeb}u{e|!3u6L(+Yt`BAhE3!U3 zXMH$;TvF^mMz0-tf5Of>?99aP8~A$KVqNOE1HZHJyAQed%on{iV-<4g(I0`I1mriJ z{A$8)1iviyA7DQc@-2wx9&$yocMG|$jAsyWJi=Zi>^@~(C`kWI>@I5#NzYTs1Gyk#>|5)O0i(EnM^}*jo@-In$BKq^e zKZbr$Lu$_R@pBBrb5$Se z6+pedg^mYZ3(N%S>6D!q$Mp2SXP(8xPe=T`hChdTL`Ciean^;u6@Dz}SJ3g8Z{3LR zJ^X6;+r)aDFG8U2B6)44zXbe6$j`%WI`*4Ch_65PuYtwDQrN#i{Fl)y#s1h0`7_8j zLcb{b)A7@q_*#(9e(XP_zc?5l{h{ptcbPBs$&d7XHHed6M01UWF-LC&dReK{cCMEc zME*ST8?Y0Fcu!Eb82H_Xy+qi1PJd70=|KN6=+D?$Mm^5bp9y+7$QK>{Zo>6y#`iei z7h-bW@QS?Na-KGq{y&NP8kiTkve zc0pkFgJEK<7XiAJ(#%qpjV4HH{nNbBbb3)5Bk@$e{aBE6!>~7 zX>Rfv4)$VQErH*J_|@A2cOYL9{zL5cMQzda zmy`b3^iRWHE9^Dpy2(ZCn8B7U35^@{R)7#t15pNcT;V;Hz4E&p%=lx6_gQ>?9>Klqb zy~U&pcpQ5_VXq-|d<0F{z9z^GCeLEzIfJ<7QJ13NHPx4VMv>1A;?Ix&uGnA6e)0{9 ziOBaQ{0i_7fyu$R#Mc#!L;pMGLqF)t&_ltS;4A#h(tN~z9@e28&`KFBwq&UJ~m0P*G} zz9Z=ADbh>eZvk&mpLzKCfS-)uD6kQ7KJsphUODtaq1T|-7C%4ZrzLa@`ZH6<|L7-G z-+pL44Y?)rqaXfvlV5fE(^Kbh=siR)5!egV(~M73hiuT}p_hXHf+f%^PTb9~6A60< z`F{C@aoB_WX5{x1??>W|1@FL)cNyqs(3*(% zm@j(E;ymn5BHt(EJ0AQU+|76tKrR~bv?HE3@GFCQd&NJsY5lkrc-_nHjMlQq!6(0a;tYvN%jeY>bvVf3=0myZ77*x7}jc;H0xABRwJ z=mq%AMgL^@iO{=?-gD|S30z6sV~M*DSQlJ^-YWFEpmz$r*~HU_s7l^{JsUNU~f6`S7g6xf!u!Neh0(o?}D9F$WOw4XY4mYeh>Uv z;9GDMdY4!iOG7V()>|(3lgE7Y|H4jM>XruodfQbo^7s`yJF(LYy}{J0CVCs?hde7# z$L8q$gkEFn@fJG;=$}FVPheu=d^39M;rPh`esEBe z@pqPsV$^pkb^@3OEAbnFxMD!>Xa8(M9eyGH%GmwHd4S$xkrDY>^e3dwE8*|JUoZUq zg1@8ar$&D`_#5$O!GARTN5uc{;8^$%;I~3ACwf^p&)CXhzLx$W#226b?(`?cUSI6h z!~YTdhp@i3#BO%R-3NUJ`Nhc3MD7i856Ej6<2#zXLdYvG{gLVa5gY(cC*Ba^J&Ale z>?eZ%o_KB$-(IjHet(1?jXI~M&Wqp=g9=|WJzl*qEFur;kdPMkluvb`m ztPfR)D?NH4=#55h5ONKP>kIW7h`%bt(H1NMmO(B9a%b_IiM(nt?!EEb4tqz)XE1Wr zkXwPBt<)(u{3-C4!0!qF3Hdx^@!kwSI{p(A-+kiy4ZHiXdk4S08Rt64XF>i2e#+tJ zPvom3Uk!iV@z;_Qz%rcwZo|)A{4_wnF#39$cwEN25BlZNCsp4L?CUKzl_2hu?-=r( zhTaVFY=He8*nNopQS>`>=2{DO$3u634kX^i%%@t!^_qIzWEiu9Y0w*rp5Fcx1$*V_ zzYl*9ab8ot=nqH#9JmoY%z4-zqUkPAjX8Fu=CQ<3YA+7wY#BnVZ`ooFmH{!WYynB#4OMfc*2Z0^P=Qr#Y#BX|zTbXn{4!S&a z4R8bgQlh6n@<)Jf0zCk_A@qOPZ3z7h{-cp!DdM_ATrZKUfPNG5c!K|m)W0U{)?4&0 zq1PF|h~)+-Jt+H|nzmd!w;e9J$-nzYcQm$*U&(jPMs@|8M+u22X-N zf`Rx;jGtf0YY%?rDS>Yb|BWKv0n{ZD>t8GUoJVdGa;xzh1lsQC|CX1$*#8x|-1sj^ez7>;i4WZ!y{6z^urT(L zVecdLUWEJ(;?d*R9^>~ObtsG7WZ0cgJbz*55c^99@-9z*72;ZrognzR(K|?;-{QX( z{^z0B8NG(|@5Ikp{A|FF-denZx(`JE0Q%F>n??Rxkc)wxA;>jFZYTVw#1V|0$I$1I z-$MPLAy>vP$KPhb`_RR}MD$-~9!y5R3-WdF(-%L5h;CcMJOk7^gcN$8J%V z3D|kcdB!2^&cJS7`lsUm1@e8Em#@H~U>I`6$iF0Z(_;5H{J8Mjz@N;16AL_o{qy+C zg8mZdqQn;&d#$0@F|YGeZ@uMZlIAV^Kj7EHUP0`gLVgwXI>opbpg$pYdJ=bK^pc=g zlDw{wR}t_c_z7G>z8A5RoqXCLe;D~+ptC{;5MM3yqC>aGPXpxtK`$M+2^@vr*7!{Z z{>%O_iTs8-4fs6$rx|9`8 zRKQM3?A##V^W=LB`B?DT^c^JLsMvc!-F@(DV5c~7UnlNt*cp$V>vXl@d|?H4`eEk- z`)MQUcaL#th>_;x{}b_@PtEVIps!(XH}MrkJ`VCZu=f-8Mnl(wZp3^)!#Mp#eE%{p zH{kai`|A|!Jj2dN)t~)9ZztJ{y+Fpj0`jSdVrkg$+StjDoyE|9LnlRV0eVH?Cx`zV@!r7S zI{ZY!PagR3;Qx$#O5#n;aWEzOeP-w}=odq79{g<7_h0IE5&v1K_d@ue;O8gKRE+Oc z=seh4PycE1j|bh2{)hD6B#%ZMCtl-sCUjiRPwNxMG3bN%UyA>$=oQ9pYy2L z{^8#s|8mgFpsoHZ=qKKy@cp}WIio{{}24x;5)DY{b$&p?=yc2P~Vf} zwURvZf=Q^yc;-`8#``n&>tMGObSCJP`0bD1GT2#$o%GDJ=-6k|Hw-(a!CPQX@G$s0 z`eEqD0lN_A2IBdSJnBMkg^mdQJNk3*o09og2s$bK8?hgi{{G+9$yhL0Bk-LiAbFeIWhmng&{_}`0HvRX=Ge7ZM08@ZH@KX~%VbDI-Rw2{F zk6hmMBjxcUPZ0GZmsx!!k;?#<_Vk3(6G~4g`SM=BQ2j#n3)No*l>Mrp{0k)~l$_A@ zOTIR=1~a<21bLG=sOFI2x! z{aw@zb_FFTl$=m7i)Z7cCUcbn??e(LCANPYX+VbSn=_gYv6-2t~`UXrXA?5iJz0I7ACY%dTi)Sx|Ab z^-#3zh!%>L9nnJ389Z884qE!{JS-0eLTfyPq7|oTp=j9^EflS|L<>dBPX$nRJ9tTHQ1NQM2t_Mi(L&Lj{`+ZR z4QScz;h|{R6)hAkyP}0PLCN>@P_*(DEfg(1(L&ME6D_Re@q2kFT71#M+Mw#Mc`OvI zJVXmcD-Y2^(b^A13q>n0(L&K0AJM|PpyKT7q4s;x+E0X{BY_P-sG zFH~K5!o9z)LirWSuTcA!!IIG(n5 z2AKSGj!)9lek7EA zh1$Q;gWA7@+Mk5lpM+ZHR6nhALglA1RE>nnCj+Q_go@9Ny<&9R;%k3&|ah1Wd(bq{4vaSR4~u#D#KWf^X7jMJhpLb4C_kZS z*%2)it@U2CFoVZ0>tPlTD|(nAn0<#PP_*<# z3q?y`v{1C{h!%>@=FvhOFSP$@tVZ9BgFJHEEXm!5b|pSMQ%X`%Y%M{A5^+;$nZ zL*TXXP{)7y((zxYf!j>7GHU2UWo6u9pBjzU)$A?5q9%X zwBi&k)OnCl=RuBE+@gi*7s{UFXbL4kzQb#n<_g8J@^@$d$U-5_*idLOP3w2(X7Sws2P_*WwXrXAyiFWdV(27?)w=Fr< zL1QAm+pgou^zl%%;u0;?_acp*z848a%eH8tXxS7k6s3BbOW&u2qGd<4P_*hMTB!Zl)zihLbAH(q4h=B7T34m7byX-jvPTO= z%b#+TjN5j6)xz<$?fA}yW_%7)fnp=kAq7HVA)YOQg!j$!lBk@+&#n zaoghS`@Y6ie7EiR&ae2+ueP0Ex9#MeU-6w^ZOfmtuWjx3LhbX8mQB$@^-E82iRZQ* zU$Hp8wjJNukbHPMf#Kw}?c`kyE=KL=vZwu0C|WY2g`#Cwv{1C{i570A={us3w^sg- zmMzC~+mcg$j_XI$?d&?f>^ga8OE$Ev@05}kisxwAay+-~ z>^i>e&ZLPil)j^-<9Ke{={vsk^<7qcq4XUs9mjLqPT%pv>nj(>*Y<3h87mDX-*>XI$?d&?f>^ga8%h{bvpX7z&Ia;Fo8`@q?le|zoN6VJu zxov0H@nzS^J6q1~X8NQjl-zcY7K+~K(L&L?Jz6OGfJX~OAN6RV=#w5T6fHlBue7(V zINY}QipA0D3s1Ye!uQLjqt&nc9PM%x?bal>?fg2v^CLREU)S&axc=~Tc)zaS#i6>X zUw*ai{5rn#>-xge;q!I<&acxC?-59>37_xkaDKJz{5rn#i6>o$Za&J!G@5J(WkV=Eq2#pBt)ff%J8903w%z=4e#CQfE>;~2 zr6ZJ_#!j@*(Xw-ZzlV!26kjMC@~=3Bic_fX7m{_`lGnDASHHIB(xfYtu26bH>3s2g z3LP!IqcrIWr6+Vg-L~Wp(KL=ijlKGuzKdZ6JlPV;mQXPWWn(E#x~j4GLh+k{CqeN_ z)3ohS{ccx)YM$Z1gCpA!^2-Z zyzJpc4+BF@K9Yw~Jxt?aIuDC@SklAN9+va4DE{TQxkp#_u!zSm=3y(3-`c|-9`^LG zkB5Cd9N^(V4@Y=7(!((xj`eVvhpRkX?crJv2YEQy!)YE)_i&4cTRq(7;dT!r<>cI& z_FHDlw<6oT?b%n^!>S%u_pqjiwLPrsVSNu9df3>*rXDu;u$6~xJZ$G-*KjzG?H(R2 z6y4jSg`)d;v{3Xwj~0q9x+a*~6_KZufAfhr2!8>)}rx9`Nvxhetd- z=HUqs&xb?BbT*S-G1-;+7ShZlWwd#Dax-gANJMjIG3yIge@f+lx{N8dDe}_f)&0Kz?h~3`UjmQPgq+DdnhHi>QT+eIFecgN^ zJv*9f$KzQeT9D^(ev^dFQ%(5$5EpJcVDlez&f#MWUvPI;$37SAj&QO05Mkuy^ zroRbs1aP6OJat`zojufY(|7!C8M!am|9u4cwd8sT_BNp3o>)%OhPT5Xxpk%gB6{bi z@!NM|F2Tjbo{Z@W>UtD* zF$QVSE8m^_+_@-u3jO!Q+m(8xNA@MLreWMFQ167;`GNKmeort*4l~Be<0SU}BR7GzG^Vc9YYY7L_?b79{TLs4r!yz1aXH31(HzF1DmI8=8#(TR ze-E88*tt=SSg858jN@E#)mWS(<`R9FTf{hs7$+~LmgJcoz4633kh%Ac7RHz_=)ziv z-}=;b9LmYLqBw~;3F|{K0`h;xlbggQq(CS_18F$K_(M+W0HSs%_n?S z!Tu`xe`gGXv40=?{mJb-wYf(-Lmv0h`?ewWsmDK(c@&Ah4XzG&!{cXfmnD{H< zzcThGvR__8t~j|Ktj#r5=3y^nZbN4#uTy>aTOc)WLTgR?j#h{9*pJPh$R*Ag)(FPA z05$%X_KLdwL(4c9UkkAVZzp~>Fm{it;S;+jiR~e+I(aX|XK)>^-O`HVdlju!BmU+< zo5A?DqsCKcZ)sH-+xxU>#5bC{_9dSr)Gf3t^3*L$54Ne@w0_Jp;w(=qN2^AwLu*W% zOq)V0P7R0C4pK+W>BvKv3$z~O6vW*7V>n|q!dhMCP%P>+ixxng3(_vJengtgeL>Vx zxj!Js|ENo0a_>#tFH3OWANuJTlkMc(jy8-MloryClmBO$@~=&sOS?}~{kPIe)#vX4 zv=$AhC1ZDyF}hA`&%9etyM~V{)Fcv3^=U>^jq)Z;ln^J-dyx%rCLe=NSK{}-C%g}Xdd-vyfRqKB6}yzJo>4}bOW znum%*dN(}0>ESI8Z+m#x!+Rb+@bICBk34+r;S&#)m+U|DP;u~NUn(uPhh@1Ju^3HQmo|cSk~WIAmo|}hm^PIrzP4T4 zj{0ZE}y(k-LyFJX8nCDy2Qt(@K*D`tADRm#0e#7q3sXUaN$a zY)__Dgq{s9r=7v}I#6rbooPI`0t{sRx`TXSunesStuHNpK3m`1Iu3*oImTP?FIq6` zauQmo)@Rx(9-Q-x_JZxCtk)T6*Vt|YzRb_@kQS*S>!8<8B<6VpY+s}4T=5+(5jWTT z2L^N8tI7Hv3J(8B zXK7bypJ>B578NSU{GrWY`*M1or2&@XcvOj2#lr#M6xtTra@t1PZQ5I!zI(*t*p-yl zn6{aAnO2QsS+p!Xa|8Sbx*a%@_K@}lekjMdqinCHWiLd&&_ieoX?+k z?r8--ay%?gOI+B_B_e?jk(JDx62|E5dWKz_YZrv;-W(i_vs0q~m#eT0h!oT8g6FdjjU@!W^RI zDQd_1<7{6j#=Sc9b?Hh@yyA2U?HJny+3$27kd!tH`3m4*T3_0FS~}K*(%sp|IKNo{ zt}V$k(|g$YLNzc_nFzjN;7sw#@*Ib9JX5VF`x9+lF|Ij*yFj(Fz4$&y)A_|{+9_Jn zZ~3i5Z_Z;Yaql8H9n^QI#mwJWeYjRtmHW_XrK)l74YHAn+d0TP=(n`Q)w$=0mZC3X z;5D6}6hv+!SO%O$OJ0k5_OtJfI&XOZFMS>E%jwVj29MHouKS#pBh1bhD-2*R(FWD$ ze)$I0UP4DGK^+=Wzk!_F(lS5~d>^JQ`i}h>xoK=qq%EXvt-*dLr2RyDL@Uyf`;EZlBN=nr zdRiLVPqfE0og3E#PtnfMZqf30;yF;Ai3!Y7n*9`+ceDhfk?R@3*9y$qm3z79YfjtD z_Qh`Kuw8A8eaCMLwx_M8jibdF%lZcD+%Y4qBW(=rDmwSTfN`uvY&Qd2(%San-X-uh zEngYx2ky1b7&orxQo?#o%YS+5gAq zi8s7YgLMm`+q6W!fB?NK!ah;`&r@&W;~Wac#t5tnzHfZG+f2TS1kMQv^5yYl13g^n z8|w@5Z+GxNIar(AS5EuZ|8&xLz6`!J{(hc*;D7h;?>^q2i0?Ac`FJ`Jv(4*2!#mV+ zvwecr#-Bzu>?HL?L~;zdn9sCO`skg_+!4uEYPQpZeibmHuPDC^3Gu%dey?+0HYs*{Wuqtg?gKJKUWrNmoaf82U5ZU_=l>5WAT^@VMCe}?y? z%Sb{(NRDHsE=|GBrtBeXRmMzIUoN%^tAl`*NO9DuFAq0krRCGy|I@!v|2B`C@>}B6 zgq!@B5qwG#h~zm`(0If3nm=*$l7;lk}=GDR>}^B9OdjMID1?gPon%;_0JTaYqBW#oMnR8;jg7>TedeB8fy5 z*`cJs$KKZ-!f^8-QDdQ@2zlVBe=9dud6|=+6P*`t>SI$vmXks8sw+IE`yZcWf(e-j zI)*<&jjN7ESsW-zK_xd4dq=s-rAe8NfD+Olc2_DBBdAxQ*7EnO z@HWhu*iz9{NS+9Uo=sTwe=V6(i0`L0r1BTy9|lPkAQj&6L-OKUx;99P&3M72Z+)5q z(&uRaY;pV~Em;ro7hW9MkLXK<%}oBi&Md~EM-lKeSpL_5rt@2j31tVS4zcA;tPBKH#ON95DC6?Q}aJ>wWZ|IK&_V1{qc0zn%>;5D2;Q^|lWvuvAdEP4Gk0hc$8ZA5G+xzB)7L{qaZO)b%CrRpbrm-`cx-XLbFD`v~T)#^q`nThg?#6j#<%XFGD)9MRm@M|!p=N<*_lAC0&^W>s@qQ6%(7plZsyMkm-m zCvC23E^DT1;ntSsvihZI9&9Wbg?%H){Y@ZG&O=60%32;}Gms@xv8krYL1PfbZw-Rp zl>YxUH(lhGNhG4v;wcRs_jyh*HlwnwsPs_{RMn(xi>tb5OQRHs5A`Y+tx48ra<)!< z)iADRq~@`Cw;EfIs;`+Z4couDCg-O_E7u^uFQKAR)9MnDIIaGY;~bX%RaHrdFN-`C z8`|WZRrRUb`Y3OkbtW%;-uz>o=tc=w%gEloEDi0|=E-VqbI8Uf5|QY%K=WPI(#ocl zT2UzmeKdFFQHMQ!Z1&kKveAr;e%OvNjJ)1U!$*bCNuqW+8L@OUC1Uzhl$%uk0fpsv zC3Jzg-HB6NziYl`_>0Bk3>J*VNsx|3I@F2aF1jdQiqZXk!w!qxTBBnTtEN_a%fl*f zW2?~YV3|Y#+B82K!yvexfxyY|}WL%?;4rt+m?D0^O);U$Z@2K6L72 z-gF$~897jz)vBlMMe=Ry4NsJSr%zP>JlFiSf0_@r3fWwY>DRXtDap%&TQ%h+?8bN^ zTo`D4-ju+Mu{dmyqWL|TU#pV-w=J9NHVYzQOCQz7_Akvvcl0xkBsAl+XKFiyoE5F* zqP?A`p80D+C!eot(brX7e9KH#*2n(0ea7~=)PAmx9Fnx-n8mC*#`F8sJXQr`gTemQ z4T4^s=!);wCGo`3Sco4BtE#V^#h86rP@g-qQGTkBj)<}Ry0-7@oFk%trPKZ<3B|6H zSF>-KnXVg6?G)@i!zO$4$Yzmcqczsf7p&@v(W(*IACY1bt=Z>#tY7+x{dI72OLo7m zeA0BsG>tEB-sXRO6se7#r0pmv3|l#c2%E~4n;bTcv;dn43qfb~mbQwfEi)t|mbxt= zsp-{vU?1C2*rBF=hnx1=3xfQ6i1tF4r$n{V+mXN>?d&*fZsbJax$l%VwpKkGWmQW@ zTg#HXMoh(_o=@lh*y)7Ds{i$odwnb`InqFg)^O|4A9hMca#*_Ru!5M#S4HQ<45svj zjhcqqW>ybZcV%tHrR8#wOUXz|v(r{!g%QZyvg45pPBBD9LbQ#y!qn)i*+<(tL$tk4 zwUDHSRx?~5H)NKF_Gw#zv`**<7q)*ok>3k2uZlzGDxwsj#V*Ps)s}dU(z@_L%-2NmViD)sAA8hh~%6x4Nhfu33-%*AYPWv|_ol zL-x#dGsrZm3rmYc*kQ5O`j=EYy_C8w5eiz1r0oTo9qu$igK0~Q9pYU87P5t>aywdO zQ8Eh1KC-Nn7!A6eK8vH_uwvTu)3$~{l)P()v0X?*<4!bfI86g#e`NAz8n$$aw&-O; zA6c`#U&R*vwdm>-M_%>aS`p}DLtxu(YMZ3L8+lbi5q_01|C($zsjLpRkV?vA#g$}y zf4-`%tFR5PtZEu4%dii#8MBH!-3(zHX9cT|&J3+ovHc-9t{J!CFyrRdIS)HE2a}l; zkL_IAk?gCgEiab4)l?@}VgE>Brhlig>X<7FL^kYzYx&vuX*1d{t4?SH4=@ z%#Q8gwmvIGTl`#fR_CuCby_DMRtzdMZ=Pl~(b))FkNHs!woKTJu{mk8PZsU4su^HW zy8>&MvPe{7N!re3K1ADTy&HKLaNOc4%Bms{vaI=LwH2Q|@t66;T+l*rM%1C*QcAYN zSwY>Pn6&*LHhlpG(pcQKA30z0ZWWX$r9L}H#y~J8Pe^K(M~|*Q!AQ2?%-y2rKjHi8=>`KN z!`V`wK9-qcw8;@R;|Ip&G~=RvCnLL-r5)%9^6L z%392}EiIEpjDF>;OC;7Sp1vs9$HvI{u$9pAQ$%ilnx^&I(NW$l3L6oNOWCT&Wh1mY z+6=S&ovc;BGB7FAby+*_n(eY_wk;NUm8Q)Vji;`FD?%FsdA7(**T%tRODDSeNY0`a z=j)dFGas@L_G~3%n&qnj6Sngz37EQqR1gX|Y{`$POpIWC`u=3v7P4HKq=h2d)-NlS zDA}>)+j=c&8+v(=q)KKML`y>UZFt;}TYQo-9m&Y9=_v>GxIQb4{yJ9L_59h1e?N0Efxmf-0KGGlTYCM$-ES903h-FoL&hG&_ z2W<`go8NZwjMDeScZB#J;cpxM?$VzlGS9%per@6^f&K#Y=R=o*9!)$oiRU_%D^Hcm z;cqjits}@kK}?>dOMevlSD|+iy*=>j6K59qpRm6l`})m53G&f9tOw`dciGgh%4MFH zOI@c^&mSZ3u65!$f_xSFf24mmdY92lOPw+V1^R9h&p_h&54)%F7ZLt8Zt9wz%I9lF zJo(5!GWn0;Z-LVyGH%p6FZF(OhUeU(Hy3-uuop!AW>ddX_|bK`Jm4g7BXL(D?r!84 zzzW$L{f6j|K(8WtGokxJuSBjia%s^Uj9yLRNliQ@$$ugFAEv)K{YCH-6rE?ZlUE_~ zibnj~;ddhMn&iC{y{_oB!2Sp3$6)%?6X!_kF@*8_k$U~g_$LNeQ@6R;yUqNHfxX*| zUkLu2;lJQr-eZZKm(-&tFT7gC{Mv$@KG;hMHpaf*IbROFM8t6ozsr%o&V10je%`Tn z6k%NZFfYnr_h;~Xa2D7GERLP0#9bDDOYm2n_zM#MG2&?*Bha@Qdqc6el>ASV|4V57 zZZ#OZjK6XCtp_$Bz9m7ts|l>eZ!njV*D3NVMt=n`|B>-!-q&FKN;7`5 zus0Zc3&4?JA?!ZDZmPGuH#z~&vS<97K^G#fD8%)GI9d=#_BX5#3Ha?BwBCIj2oB_i zy=Pz(`gak}MdryK>J*pxcLM!c=oiEOW9*&6ZhOT~-ER_4Q|g<8`Z7KJ&k`<=-ZJXj znYd~Z*ALj6&5ew^nGXrzZzi8O)U7vk750}U89u>)>Kyuyzg8q2?)Wu#o?7YEFAL1!PJpJL9h5rh@cIaKi{|5X|An)ho zy#o0v)Fm5v=OXVT_@9CQJIL)t?md26u>L;7&JNj!Udgk(*OK2`#^*Y83i=;ouM_*_ z75KRs$Ae&Ua07lHl6Pw6@ArH!Z9x4pP`{^4h^zRGhrd7YGln>FKqo|g1?#{ev|9PZLyQ_E&FByp4m-*UF`IO-xGc@5qV&IgC#i=%&Q;8+2y!n2rC|_`k|{tYAEvVLvkVPr{!Ke+=VTn{f;V6N9CR zGaGRZp}!LSNudLwqu{qC^{hbLmwC~52k_4r*hVf7a)XI;D)mTCeacXuz4#lCzvb}% zgP#}tlQ`qxe=m6-!(K|_4wEUN$2-39et%5Yxz?KVn8%EDp67M#ExR1P z-S{i+d3B0@4(ngh+fT2Ee19R&BiOfPKbxP!;{A}{y6%gO#TWi&&;`%++V20~v3rl* ze0eCV4)ev;)A{a;|8w|X)|aQPZ-zG+UVZrCZ$a;-_a42E*bTJ*jPV>^FWkdhzPIt4 zUcU~bKY;#y{)agaP4)c=eP3B!y2toieDawmot)v(0FQ#^f67N(<0r=9`k;nBcny9- zex8G$2L2BEbLjWxr;Yfwxt|@d-)5`2i?0ZNH9cQnlCO$*{qm>YV%I<%)x=R!-V?yj zjo&f($}W#1LjBC&a9?{pi{HOguSW3Gz%R@HH8{PldIq!a2=`73vEmUQk2l!;CC=&Q z{mi?uKf=B;y)*Ro*}rRlwY>J0*Td#<)ju2j)5cG&Z?K*SuP=i)vi}V3MtCXhzw5p+2fxAiO{Ld^ zUPg8KCw}~>vhtk>ZXNq2?4M@ekNpVvE#ZHz58g9Re9zz8@yVt?+)8loS-)bvhrBP6 z_d)D3n^$r_sEuDIejlkr6Z7`w&*6W8pEu>_ru-!4_j7ePOz)C@>MH(H=3VvaC-6SC zo>W{<=z}cskcR)g?qj9dFJxa2ZcDg@25<+dm=SRpezA{HAbn!>Pc28T)4PcwHZtFur44j>jQ=aU0*6 zaGr-(2<~w_b4U5xweX(i?^W>*6YnGQE}k=A!rcV-9J`_HK7`j>yi4#MuKpwK*Ro$r zJ~E_}L-Tjdd#cM2bt#AMZFT-t-(6>42+kMmXTZtjbIDGAp5*s^epm3@RbKYU%QSgv zDo^j=xf;)F_V?Jo&d*MM+OzvW{ORtCImUh8`gZGITQ30b8b9OsE1c8w+DGz+>VC)m z8}z0dyBkXx_sU-~JSVD87xkHBy@B;j?CY`LCU3vW+d^@?D)0S6J>hMnKc4<9dL`(+ z@weX-cK=L?|L4wUhI?|W?~5A~;Wry@OSmoN?UwiaYWQ3=KVp5ZF}u9o6<=O>XXU54 zI__4-x8T-*Ta^BX^d7Kp=X}jIc9s8Y{B_gEV+wc<>w}VbmBFire3gaQ&i)qf;rHpc zW>*jHX1LG7jpO}&wfpcU{1cv&ALHlp7ycGczk>1)FTVV=F-F_}+x|Uun~&c!=4sXC zH~Ovh$t&{nsrvyZY=V<8@P=udy$}Urzfm`o9PK zU-WAs`_(;n?>VuYuEWUN{-NfH0@r)49)BJ@0XRR%~;_&9vD@^Z# zJ}4dXgZ~=1=isOE9&*w6lrgD1do?)|4(8H(PtbZwUXM{detuUSPjbpKlk>2=(~Hc^DwROy=c`H}L+Kzl;3!$D=Jw*){Qb;H)u;eky;@cSKj!xdxI4XH4irZf{@TMi1gE?8#Lj6k zc_<|hH`(`ee^~;*3;eRy%UQpuZ{C-eqVn=q3eQjbKf+0FKgRwXIGyFc6u*zeTMgfT z@lB53ML473ex4vf)B}0>SYER5Ka~G+_x=4*aTYV5Wga8{@5uXPc;RmomA2pBekyhk z`7dO>qQ2NCj>-IY)GxQ<#EaT)KF0hkz3uR;$X^V*x$?Y1p8w!~oVYF;+Z%t!bBg}X ziQirP`ujd&y869iK1V*Qv8%^!IQ@_GPe#0s;?+!CPwT_O_{P{z&Hq&WHdntb$L~Y@ z;;7$Bee;fU*a@%Vcy(i+%Kmb8_r#gl`gJ^y^S6k<3;HLIzFK3w5B+B9woBb!_c`Es z<6e9VsApCF|CZN$^!$-{|373@RCaamV_w0$61)`fPMR+>Kcqg3qrKm%SNMOP;~39c zpX;2o!F!!~KkK8^HHqi*6z90Lb5K?ubKqA5-x>JU<2U^6%!Tr@QruPGr-xHtT#w{s z8@&Y92g9odue`jx27fF+b&Z4Ve+XwK+zD_?^LJDqHH6oOT^~3x^7EQFQ;72&aTOBR zAo?xoKP5lA)u)K~{((QnSX-RS*;Qt@PrkmErFpMBVU!R|b}Verf1-PQdlhkiJu z4~x*7MX!oF)>X&R>U>_EJK*sG9*xBP6dtAUco&a^@HRLf3E&*`TpnS59lrmiBB}+u z=JfN@{}`WD_>9p%b@Wdh@s`9lGrK1e#E&|pPS5F+pXk3q|2#g$+;>{bcLDjXY>Y9k zrT?$z@jK@8%zyVebB4U7hgZp%k)LnbzbcNi;y6SildJF=EV0mb-N*NugU9c?q~7DyVCQoEx!fC zR~4@ec$E`JQam2e|Au}#cz+qMSl@1aG{1BB-Hcxnee*HA1oHToyl!@Gv(rz^e;xXn z>DS{wAOG)}FEmeN|7Unxj5Fyi;kOIFgYo%V{#S|TsD8U>eXI2{zRxJCp3C&rdUi9^ zcd>aQc^R!9?bM}`v5fO}E}hR1;{HV3^Z6~x??OBu$a^>X_u|Bl>dtRHe*eYiLwpv} z|B-$QaUAx!cb)vdFaI0N7keJPBCcxs^hr1g)%&h-h`7I!w^#T}Wq*f$+Gwm2`k9}H z-ZNfizmomX{LZoSJI{F=5bg7|F@rG&za#mLBhJhCq{8Q_c(20m0)K@(yd@9G=7w=R0BPadvH)Uh+yMy0EdiB}eHHN=mJVQMe@iWAE%xWA2 zFZ}J%73#BIeV!KISn;KUvq^u{us;*;>G&>k|6X>^JsIv4aW~b(-6sR3aBwuf-;|Tjh#CcQv#XV08`aGCRes{@p9Q}~i& z$IW-ceL^0$z-`3eV)NVllw+4tJsZ(`ie3?Mz2SS!E$;u}hoN7?FGp{PzN_c|{20dn zH{$yqpVs=QADmP656e$}_m`IDd(Df>(_HrR)HVEDCcDfpn18}9FS|MVtzWeFMfern zZ(}Ieus#@ve=}o8{S|4LQ9=mPqqU85G zI5GJ2!>6b^{Hx9j;1#hy#W`>6od2pGUEH_U7^mygH1|DU<)bV6y7HG9-<|rQqI0;7 zep&PH&5Of}qpsgLA0PA6n*9$x59g$})Oo$94`#?yCjIq;ygrhjvCiQlxFg}N#Xqq+ zwt-hvT{~UH58fjDis3g;J=(Eb!%r{k)x}p|d^PNUA|Gqz^OSsMg#SIg4*Z6{4c=S) z(fAx;Ki_&=eoo=>Je+iJGU~&3h^tQmeq`pOs;cwwCz_$aw z3-BF|ZzKE;i~p85_rU3{uJ1#p)u`!XWR2?9$sbe8i#Lc_rDa*OZsSM1J46^{)6Xu{r?+&Bg9==ynh;*`#+;S zXXP!6ydRXebn^FCDnI|kXBWMvyr-1HD=S{p;P!<3oA;3Ecr|9Xo&G_%zlnbeKMkC( zWBe}Q_X+E>tFr(Y<$mE`Mza;Jpc9h@1*a) zcP@9ZPXwn0Kl$+4L$4M4(&BCJx!Fa2i+T@lVLh((?(`by<3s!$HIIkSXLz;d|2yYm z6}(^Y`9eGy#FGS%b$C>!mm2RV^Eu`Z%}(Kks^JWdZSJ{=6?@ID?Q(fLwmu39Maefof|4bbB`0XvO z7I0sHTQiG)=S4rfLN66Q8N~aYJl!$AMSq+9&+Wg??xDPorguvmxZXx-aEbcqv{>E5XKI78=n%-@E zX5%vuPHz6X$k*HQRX~1=$ZtveNAz18b|=}7vfe)AUEF)by_dg*a0=u1f<8WuSI2N) z;6MCg?qL3ViR%^qhTzu%&M`PO)F-|AbfsTg92@Kpv_BbMFL+Dn{Yx*6^&id0i+f=B zJr#Zu>(|%WPi4PYy;iGNYWcrno?aX`-N)B#a-U*<3jc26t3*GQ{Vex9tGp+Dh*wT| zDaI};ozGQQJ@bs~`5S=8UU|4I4_~t%!~R?L``Kp}*AQ{t;HR{6*VlUZ_t&QD--YUX z(tL?|L-qfYU0it@Bu}sLcR?Qh!hZt(W#KG@^A-Hz^yY~7cYL$U$A@_Jl;?)gKHuVT z6ps#g6~(Kb^{Uo?!TSilL)ri9{H)?X{2Ot3;T(XIKtE^THy1v?up7p{hVc^pQT(-H zpO5{ab9zaB-w@B|>Ytw8&*quU|IwFsjP>r=$0;JKldT@jIVg zd-yfw@h04N%@>&GHvSE7Eq}eOzhWF@{8S#A$wLA2Kjdp5UVn;XGQN$?Z>V==`6+H5 z&wRdk+KT53eD}hu4L39V`o>jb(Os|G{34Sv3Gs)P~m|xxtMSCZpzsG)N<8kA={LJI$x_lIMu3k1D zY~EVkzw|k~3;R0kpHcr0-JjN)Pc|=(@5kx<+y%d%@Egv5ae3{|{{uXen(vYSPV{Th zf5LcQo-^O~E*Rt4gU<|n8t|8czv24iJ$-VST^@Z|)_uH~dd0zW3!dfZm#3eM{w4W3 z1@C3JOYA=fcdtB7vVKTkreT-X{-4&*$;J0bdHi0%?+3Uu;9fP3GuFW; z6F&DG`|!8pUsAUx)UCOCol&m?_9xqaQN7YSuUG9Cvfs;i-nrT(zIo#7;ym=1rwaTJ zbl+fs zy@+6L&zGh2qxkP9A6?~TB)uv0a?4*m`MZzr+xULVPjmS?jn51ECKvnf-RD}d%g=70 z^`r8?60ZmFJ6JDoJ+5&9yRGUl6_2&zs31=@jh{M?z3?oD=P|spvzyD$x{w$7-6g;E zjML;kXhZ!G^^`2Wdx%Gi}%O7;E&kNGLmM7@W14skcI{s+7m{x^zeopV~o`_>cm z*5muSyf?F75|8!zy-Vn4@yEk+jrZEI^cJbxets77Q&%3c%EQyvzhIwBd^y;C%wGZi zj_{M%`n&R6%=<%jc**=8<7E1;!1d~TVAp zw7l$w(-Pi2{D!*UbjK$?`(4g;_;*@c>yK{w~C4nK47sLJk& z{deu(5br?o)o^a^sOJajd6l0r@CM==U)*`jKVer1pRD?`EBq4d$Fr}&eh1vT_DiUH zes$Q0?;w2Z^P8RDAJ|WWw~O6acCF0goA1#VKk5I@{MP0-n|$OIPiA^Qu*=K;3VpQF z`e^GL;SI9B+I!Vh{Xf|EL3KP29`e7&??rZIR}6kBygKTy((rHU?~?Gdz%PYgLH%<$ zo9DAQ^5FT8^-A=9r`JqBX4Q|gBW;>Jn`TzaxT`hAHe<#_{ZU&hr3=J zW7PSg?`J!U@2>tB0_O^x->i>P|3B!Rpx4@%-?$lGQt!R*t5Xs2o#F3kafA=j#eHs_ zK(8LXS@^Zb?`wI8r(X5=&&K~b`FL5~d^sO=NnXkrQ<@jd;`tTj-Ts^&w|>B*&u>{lDb*N#lFQXt;NL z&iF>WkH)JPkCs9*ABco*{?!xCx8F&H_3UJi^n_S&mjKz zcrL-G1N`mIbzJMM)L|$-)$u8BzQnvdyru9q!MP6SSLbnBjQ0-Te^jx)R$pv_SDsxl zeRKhz5%?65-xlok{&Um&nO<>tZ^L^;zYqQ5^3vGz?-snK@)T`cFW+6P@0PDE{KbP;+j?^6 z^(pJQ!u!Tm_fvK355EWe;&?xa_d@aXa}LJvlic?`U&+g>@=_VEI&hY<{~c~_xZ}i8 zg#B%POUYw9c|0mV*{rwYuavs3q4%5lC^)s@v=LW9acwalZhoD9CHuYg)wB2%g3}Dn zL420tv&s5k>-+gB=6w7McNTw_tZ%kn()#b_P4NE0_oun#d7nJ5qIa18>FV7~pEMTF zRe9UMu0Ol?^leXlHW{DB_%sz?2l0)8Q_=ZqX1wA#F_vB)dUfnKvwu}THInzd{9fkw zRs3)1x54sq!gHum7T+V;uSLHJ{b|O9#`@wZBc9>TTUz()26$w{WAPom9PNH?KF>S{ z{yXuX27iaTyx}|?Lrm<_nt{ML;`R{GLy!CqcAH~0}I)88f zk@GN1UR%n;EqSN|Ckvdk>eE~Plk@kgcxqXH()tQGJ>YZ@UqSu;kNFw%j^cY$e6`_z z8~oy51OJxzRmQK5eEsTN#Iv5<`hDMH&lFEG^Jw$Y_Ma9{0`nB+KftfSe@}6gfmcf1 zuIt}-@yaA`{opo*`?hhFar}9?#IK$B?yC2Cc(vet&3+5}74*i_8%?hcy&ia%#rs?L z=kxRmi}Ocuc4MDFpQn+JT`9cxh5GrqYc=}m=+{uUhWu}aI~4Ahc$Hyy7w!?bf5O?2 zB5_nP{x6DmySiocp52?>Fn0a*U1fduJNskokFrn3zPG$37I$C%Gx7h9@v467%x?*P zbKUn&7Q?5Xe-HE=sE^-e{7Un4m7i_&M$)UU4(rsRKl|)(Hu5uopSO*(jY;&`827oq z@al_KUiq9L&zJGvj{kmmZRO(%UeEjfzO?le*55bJgy+-e)K4DQ!JiF3xA7_cv{XO7 z#4a)W-S|}JFD-x1;@t`F@8q$M=Sx-VsjcsVJ09*H>*>7rjdb5?F0ME2&#+${&Ludl z^g~T~8OQ$w_o*88qwPO5AA`?H_Rq0jYQ3}d{NkA;o|)qNMSoRLk3@ctuMYi`>}#q+ zV|6$xU*X@bpKWYyynoJT32_~uU()?#i8^&sr{v;0 zo5(J^Jl(-FE8J-RCRPIe+Va=be7XBXMs>Nz{uTar!jFT0I{YWYnI(TE)T6X|yvxs8 zdaaFBjU(l4n)_0cd%lzMe0!h&+;CUmSq08K^OxM;hUwqI;=I6LJN=Lg|ID6~FPXW^|BmxKAJ6aMJ&oUA{LK?jA@MvzzcBqu`mBr34bK=G$9QkUw++6P z*{x((6iyv|+R8cmJ)L(>^Sttw5bqW8(;Uw@cz!_tDS6x={%z*d{7=7q@hv64m95XS z{~f$(>Typ!w&775j~w!l1g|dk-?rZn&z$PLnqETZEFqpL@qEjEG@MWM!%OPWhFvvw z-wPdpvOGfJM$snbq4iQ)XFo=3$~+4@+|?}hAEv-?v1 zO5*Vw{7>QU5N8&B{RRDv^h=8OYrI=$@q2Ra7d`2hR-Z%s{1N=XKga$Iz5ehLvtJ`m zrRA-gJYM3jgL?fd&X2|UllcJi@#5?x&IfP?xPO$RKg4@`wtIf2;ytA+{ZjPH;q??= z1>t@R_Z9xHs`nf?cl7&5>~mS~re5*jeF7(uJhZg_41V9s`*i%*v;R%sy(I1}cn%O> zMe&`WmyZ1;{o7Fg78cKR@npd-H@uSezX|W3{Kdn2t^D_u|Idvt8$X8M7yg4n?n~@a zz`bO>q4Rzyj(>7WUVgA&K|E>2lg$1F`_GCir?@(*+Zp|_0!|rv!{F8k`{u_}z^AvB z-WX$NV-I{9doNicj&Jj z)&6I2cf!5q`SuCBt^AMR|7Cm!!UC zmQFubVc*NSy@=0i{I0^U7k-oV!$kM5`__+GukHLb)$cvTeN^0a&6}I2W?z<{0_@_k z+t1&9@xRXBBK|6hYq)dq3V(6=dr4e1@y^Pw2D=Z~tz`E|oae>)oO~>ok2&;@=&QG) zJU4t^kH&j5-mCRfPyJL$yfgH5U-)mRPaESV<7#|+<2xF^Y4~k{KN9{7e0t+ERel=F z&+GgxbNZ*Vd2;iU_Osg`%}*VE#u)1wZ`z+^ zza{>A@lOr^k@yo@yQunvu--6%7`cm;;(gz>FPl^9l`|I#~D4qpy2fmQl*8W&=R2Iiq@qzclYCFE!<5g+A*>zac(P>gQef&cb&IzPrS8*LqL9KI5l3-iyV% zmY*&9G{*C0jl91i@86#HT;Xphy${suGW%!wnT2-?yjzK5pExGdpGSWc`vmOA!XL+P zdE?LW5g*P~ykg|x9ry8W@b;=l3VeU#cP9HK>_3A44E&So(^Y-0z$*xEs<@lVXC>>W zQ#h~UokH(pV_tUM*_H5lqN4e7>$})x<~N=E9fngE-zV`M!2Xf?mx7lN-dW>1<34;c zcn^Hq{8#a&#q$833)Sy^=eVgjo)XV#I4j`nh1(kM>Q{YtXI?-arpv=@et&b{ZiUA_ zJeIQC$*zm|OTt~j&mZC*pgvX9=Y(5YKKF+oorAC7ov~jVk81QY(oZZucdZ{(zlG7x zKEHp9XSnyEq24R*@K?=!yAu6q`orX*vHEToXB~QX#q+lKHi;vF`$Hvtw4L89)^F2q zCZ4D9PXs>|{8HjrAdaGVyr}=$(7)*Wpmy{N!#PHO0sZChOT)h)zViBL5d4AiR9`-R zvR~r9{*RFxe8=K{R~%>5|4n&Gr(V%;XW&tk-;(@JgR?-MYMWOw?*;$26z+T0msl^S zKb~i|ga6U+-@<1mJ{{@TrC%5CSMl!2e?s$T`B^0YQS?3$XF)tZ$9FlreV&sU?GLn{ zNB;J!*G74}AaA?zoR4P<{5}?Uiu2BZ`De~q3g@gJy~^Hm+WG(c7P2cY&eq~AjmNY6 zuJgQa26q_#aeU7)1n=>97lxA%kA(92hR=gD@hL7Z-*5uCcJ;(4TRSM z-eLLrR=&pR!x!1zvEMb+5pGqu2l2|P4+qLq_y?0}TF+?xY4{I}W$>tq$LH>6-QbLo zm#*%EPuag`|2=rA;LVhW-13mf`g!YD)E^)Bgm&O!8SZ#^-}< zK7TnECG0oThZ*pW!utt%NGlJ|!&&P+vYR+x)OU;It&_ZUu>Onp{rLEvGr!I5eRfyz ztH56y`bFqBq<4ni8hU;3dP4rj$-@me``Asi-q3m{bxNjAIgB6ZpIhRYV!SV}^_{z{ z_~gOoH@FjgAAFkqD)tlHPiM3Hf?h85DK1~n$yafD59DPMyRPgS%EMN?-hkhU{!BcV z>aXwQYo9uv5`QIiNQU18{BFZP4F5QOi}0IaY++1myl#CfoW^iAWD%db)Hc3iOv~?H ze(UmEnctnBr{N!s=?t#~yBYeRIsBn`PQWutex~|;g%tdM>b>m$C8@m_=f0pn!j5&3N= zzqk3Xt51qqk8Ax^{1fZvRMwCCJpHQt?&kL}oLO)xUiC9d<8J!n=r_YF%Kpf}-*f8| zxc%U^#k-JrljFO<^Wj(HFnJj+@6DY5&gRwKA7;T{!oI)yysti&;ckUnm;X0>-b||= z)5Y12-#_`?&QA*G_;2>p;B}YBLGoC@{9}HeVOPZO`JXn=YQHP{JMJ&j_?_$A?6H5s zdq^8Rzfa-5#_yZ_?&o(Bzb{xnYyF>W;TZUQe9k+w{1-E3GVY`|hTboDjKJf*IFE|+ zDf>z7za>vI@qCG1e|nSntIJu1d~TmOlE8tWg*?=5ym*{zfJyUxox@w_UY^?3b( zS8aSg_Z)4(Pd;{K`0eDJzAW!`)qc($1OFC3>DldP*MeSWddjAv>*N8lgr{cwr((bntI`;FcM-y_drcN5NTI8)>;v*%S0eUpIQ z9=DN%;+f9hH~hteGuZj-ERK)Z6%+3q`@Q6`pt=lJ&#LlN9sV8of8#q5-}e00;CHn1 z-pPIDJAM7RJT`;(zWcyj`$O!f!Z#zoo8W!|_gVHm)O96)@zr~_`_fW&i{+`ad^V7u zjPetW_udrVt>vk{Iv(P`BfV>I--a94d@DXl;l1zuV=Rf7*JSZ@6Hg0x zd*JQmKgNAv4*NFrt{e9n6XCau-{tV*iZiG6NAN$+7Jg+kUQ~_y?&Ut`pXVoy=Rz4g zkBPU1`3=wU57_q=Z)@=v)F&U}vC27E&fg>FI1#-KcwSNGt?ArT*bn2cD!sJylJIwf zzpvHh44jo&eLv$qln&m0d3jPi+ug^@nE!2_M?Z|$5B>4IARh_M+nHzLFRq`@);G>l zw@Tt(DehG8zlUGR`Y7ui@F`HmQ{$&1~_p4&`@A3aO`}oG! z_5Zv4?1mfP&-sgrFOK-O@jH#**X1F#`|O|k^$YpVXH0F($^K1w`3>)7elH-O`TOR> z@P8ivLGWsN?>Ycy0h}FhN5id#&vNgX82YjOX7_;B^w%4jd5gm5BfPWVn<7D?sLA4MB);VAyRu7T{hH_G z5n~bOupPeNxSz$xYpi^~2=90O*9`u<@T2d0Ud4F6saJ1xTE*@!c{nXUEBHxBe<%G_ z_}_5uM)LC+KUL^W(C6vodyhUjVP2lUB;vk@Z&m9bS)YwxEBwyly+$4@8&evm@_(2A z3-k}jQ;~Z<*Wl3#&Ia#u4UO}SbLFY6Je`5p3y(j=F-aa9;FAoW)a-V7zKzGXKE7w@ zucn{T`YG#WtS7d9TK@iaAKYO)m^eYy*YG>JZ~sI8Bl^wNp_4jv&Z188Q0%I8Jg>3e z#XgR{NgC~a%lrZTEO6rJo3G%sh0{}Bj>*da`<3Z8V1Ju^1NlFoUp~UW5B}rD{e!qa zqhEl(B;uK&Psif%BHR`BX!zdH|wlN z;n9Tt4m?KVS3$gc_3>Nmw#fT%d?v};26-#N?-lrk@msAv^X#{={}mpu;jzp9cl!7> z_&>o<0{@ujN)CC*Cl7JWcbFH+={?2%P4|b-_!%aT6Y(7ZrzV^N{M}TqKJd%I-;Vbv zyf@+bFP^jEuXHX47^@q5i8CFXV*FpkFRgKn@v{7lmcIk^(z4qxuD9t`ftLne2X;@G z&*HZgzZK;FEBx9S7aL(Yo3g@Bx-QqVN`$z1y^1s9Vb-4Kp@OH^p5_vt$zLw|4Ve7N3pSGUM`Wf?; z=Jnu}hBrtZ##&$C{Iyfx*YNlZk1KfXg#U-}1LG+CGpOHE`Pgf0jMrl4uBp7_l9$i%3(z1yny zzw$Rg{LqUEVEzMncznZz>(pIQ9n)er6DqqclxGCpNoO>e4m8&5rUyT6{qqmMW~ zgp(gm9=P@8_ZfWt$gL_ThUNQg2{0n@B%IhNQ9r#-eZxpVBBt-0*&b7hfKh`Cjcb{hsu< z(VI=Lz5OEa&KWlt_tzX!b1@Jfm6i5TZrUKV&?8!WFa<#nrkCUYL@~9 zOZgco-xu6hPVqmSpLYBl6-Ntw*~+|%`Cj*t%KGG@{>!VcpHzox{N#mO+a(xd55emR_2~@1x;zaOM-_3Dus_d!HF(pU!?^M}+!>_?%Qvge<`2O!(Rt~0R86l)5+freg^PY+4#Hparqo;e9@Ry+(+v*1s&f7O0Zc|Rua->J_O^|`5TRjl{mZ@+qPlaCDYnSy>d z>uu;CpkG4#iDR4}d7B__=X}2CjPFEoO>^FsTOVnCD!iG#AI-*oIQz8XIqN=uQl2)- z(-m=y6-OHLKlpDcj=bWSf@c#vyV}2>0xx!DeGl1AUzF0Hv*-_GzXyIIpCik_UjVnAv`!+Dk620UMIuFJTe z46IS^VbeeV)%pgaT0x;9?rM?K85ce_TT023Gwx{|F8Q(fBTjA=^{_}m9mM;_c%%AWh1*<@E_ngjK8+<2AV&~-v;}S)Mv4I1@pPa(#9X@ouv1z zJXe#yQRW@Zzc(H?4&$c{KgZa&cJ3=?abM79Z_|I0{!#JXa_&3eSrpGW^ycBwp57sP ziRIx*am=%Rg8w}7RYYAs67O1Z*JYQ5-9r9G@b{PXGuEFl-ZY;e?sV=4%lH|s9t-Gq zpuZEZnRu;{ucPX}82-obJ710C0jfuh_tMc5XLE60Ww(RfKl-4*bF&V9KYs3-*K*%H zBc6QX`AB|OvOB@wU2zq>2haOwWBR%1FRQS^vz3nmBz2I=lnnRC%He3a37oO{Y)Kd#bpzu-`2&t>`_2&s03W6?aE<`v*>W{ztmMHlde; zpWJwSrLTI4W2iVX!HWwoHN2kiUgS3$e^0Xe!~1`Gdv$U+1U0e9WZZ zlKxRR>)=e{X9ho|;a7pb#JI-Tj9wmk`Ph|G&qR2v!sE8OS6272;!mkRe>ZPuK21E$ z#FL!<$Li7sPIov{^w(7P+qdbrk-vKQWWlGqy#MMxv6-LY{CsDBn*ELNlIzpw@rs96 zZE@`JeA_7Azr?%Je4;*^K<`KQwZ;7XDt`swtyHI)>h&z#w&JTUz770;;Xe8Yyb1Uh zz2}`yT^i%p4!@V|pS2$c?kxGc>-Wzm+Aoh+8oVy)i+Y|TZL{c4{qd}EiE$1-x9}OO zA3H{Sw$QIdzpp%X(l;aQe`^0Z{D#P1Z+^;opZSuXOiAKLRmUSEKY7jb`utFj{$~15 zTmQ*@doG+~aQaw3VSTx{x{9x=xSkQ$m2Bbb7rzItFK^4+R{2OTZ(s1&TAhpFlL((5 z&HI}VODTT$%Vp*d%sJ@ldx43$&!}lHe`d+@q%l9n#&O<*X{lx4$upckZ72<56 z{x_Ycw&F?TJa>@SI`SHiUTb<~#ghu3s^+WBZ_=ME?zQH>n?F#uDe`#){u=lbjNOgT z(R&~6LAdSV9;2TbkH5utTEC98{*?9R&fhP1H|FSKRd^INpJ{$y zo+q;(0Jk~Z#_D`T9ChWpB>VU9ZHMn1pHr5}%N=nhv;TtqBKBL-|J1xDzgzVECGn1? z_nJ7O_1Dkh8!XPU_nj+#vf}%NzIp+#7`$$XYm)q2_}sg<{4clO)p`N*zvQ>8xCV%8 z5!~`{N10DG|5JRY?dRY(wa=H!@E?KyBB@j z^YizsIL?Zrx;S>|?^oeWf>Q!cQaCZpg307C(n%f64hAjn^{zrQ~6(_-2T2iFpb0 zGWcc3FS~f&H_yY*ul%*L|7{BA3*Kh&q^I|>daK=kUo!7+-bI~WaPHcQtB1In!#nC6o#%fy|NqL-5_C`SJq(`r%U%pLoVA{60hfy8an1-si=e-1;r+GvF=8zb}4G%=^MQ38%2?lm6CroQT0j9mqGN3GAdzM9`}#XE&vA$CXceA0d%{&G9- zQ{-!oe0?vT=kfm*uVZ)>k)PV~(~n*jyoSLU3+FlbHQE1Qy_!Dk&At`;r|*0B!85M0 zwDE!ZHS$~<&fi(M!{s5K`(GjRY32{i7wU`7>iJ@{GnU1BpYv3d|BNA@)|*)G&)+Ei zdg7lJ|4saz<1aUU1@SA&UrYXS%XcpKvA@OrzPRJ?ztsKp4f&lZzu(jE>-?7gTyN`_ zb@KdL$g_C`^Ah4|;rA7K!fysY9iDyge4hUX{1BN6= zJbrN=ubIDYy@N3hKU=+be!)*Eeo~63yL{Fc?_Tjvh1UmOMf%_Im(u!V{7UgxmcL5! zeTknH;_o58=El6n*NxMSIoUtKz8yXV>DAyTfx0~@f5-Ui0q=KsZ^Q3~PZ~V0v1=mE zcbxNC=B@CqeJ@T_cAu-_nty8l1l$d9=Q3+&@ z8U54K=Z>`E`-a|ZdinLuFzZG2Rb#k!;m*cy5Pm7?U8eVm`8M--#vjFfGLG*B+!uDU zug_0Hem}>1s61SihoSg>jBiQy@!(~~<7qtd@sr?43;Du{kilu(VIvwFTcOx-5Ks-xa*zE zt$3F_@0{ZKmT|UmoO~{o&!6Dbg0n?F&&g*;b*bokk^JKNgWem~Z&>f7E`@!san1gV z`k^)a{qSeN*$3wrygtF}d2#F%#{&C5*zd)zto(1s;~*YM+0Svl-x6OJ^R?`6v!7s| z-@J`FJd(F<^sefU!Q!c6{+#nx!u>xEKi|`Pg8p7{cNcekao03fG-l*~HvbdV<0tuQ zFTVTk%k%NrfX6{Rj<8>@4lk)gZ#<9VnT!21?Dyc4&->l4^19gjN+bK*?0>}X7V(w9 zCoVn-`5EEd&9Z;Pegoqc;~9FJ>3zaqLGxSumvi4titj#r7l`W>aox`5dm{Gd<$0re z)n&hl{vKmeyxw!(%eb#xXE#2T|L=g_dU~%|Pw2keMSLHNFR}R%_k|tm^rbp2#=AH_ z8?DEumjvH0*nNQ4CcNhGzlQ(P^8A@NhQO_E{~LX=TfM)bcbMK-_E+5J-l3mRy(fxi zq|dpN@VkWP4eJf9x1hI{-dX#FtWWm6WJ~AcbMxZn<>YCTI6t%A(faT7N8{C>eJ4DY z%g>egI+a_E;`^i#trg#SNyZ>N_~-e$0S zz;3cUWOBZLFfS86&)xTVE5`2-xUXJ>^C|sj=vTGg*Ux9-s(&)|9}a&4{EqTb3~tmt z@A=*no|Bi{^0LeG=s9`LN$;%qFW{R)eP7g1E#N(5cZ%Ix{I^nv;^t47SH*X-=j(L! zXe#eL@j8cRR=5d6zU3*4^Ytd&YWiFt^(J2V?)%x7y!?a51w2;6JpwnK zd}N9CxfxCid44@gU*eO4el7ab@!1{nB>%J3KUNa7(_alDO@g0O;RqKi1O>w^{A-@&H)s&wn`7J`^Ml>=PTO8Ta5bUjF~ZW0Lz`*DUV8>bXMR?z4;U zK9e5ree@>MTMYLfxE<84Yw>!L{rBQ*O~0_bmXpWc`skGK z3#ms(`KTx#>+nqCedCfkzOIhtt~&Sp)?}aFcnp3I_!Ib>&EF|}m*bn?7{~afakH@| z`=abWqd$ax4e^wc-}d}&^L{o|U$l<)P9Y!f$;V)NS?HCrMmfG(s59`erx<8C$ zm&p14#QIw6Rm?wxcgTF2c?bC_?>Uwnj|1?(w!Y2!NO`)Uo>k-}mAuqZ&sWs5y8UST zyWrHL-x}YSox6{$ueSa--e2Ke%>H41-e#9x9)Gca)P7aG7JI+AXn!R8i|}8Cf6chV zm`NV)vromY2Y;J9hjz)^Wb@1BmDPKddS{@23*H*@ljaxsJ;-h;y`Sm5C@)pyfB?`6He`P+E^DE{{D7u&5*w?2*kX7c-!``-nAr_j5lo-6E+vOgDIYj}y& zJCpw0gWsF>=(8Fll;z- z->2DSci$VXpC_rqKI@IFXQ7{f{tIx{!&xN0A>!L$9A}J!Uw_Y|h4Rr+K1!Q!#CN~= zGI`#<#BcC?4}J^xhX!!JPT{k8O80O43$lyP?f^ea__=F;9=`|ae>J?6u& zTZRACpVuOi0VheSS`EvrC8SyyU2P?d$mXp(v#F72TAY8<6E9{eO(VC z)ksU?Zq7`+yh5fERGW_#z4GVGQ$t8O8`^7z0KGtGny2dst3;bvFNc_*3_wndQl>eE#1^E5(|KSs28BUg%2aM{EbwVD)JmflD z?;)>~R~}1P3)f-)1UW+9An%f+WI5SSHj*w0e1A{!gnxH|yzqED`FO1Lc&wX3Ey;&u z57|mqJRa8@lj^rUWB?gY_Qtxtlq&oV0xqNq2|27z=Ee#CF9@F`Qs3IHLrv{44d?%zxiM? zgk&-AXK$$Mr(K8K?j`$3$mK~AdN9;F=m))sPRMUWr$T&u;dHX!#CX>jxO6qPGjxPUAq zp+`c_-gX^&E7FT;t!Fonj8z`bL;pms!=Bs8|Em{zEMgPUNf%2eTt{@mbwnp%MsOni z8*pOlhWY=kq@oyfi^DBs{69(9FXli0EhVk}x8$H3#8#Bo7Qch{zY^ICYyXWE^ox+t z8zo3dQi_x&Wk^}_3`vvI_jAU)Bp>NZf?c==M(%n4f9yN33;iB?I@~Kl--mlr==X3> z3q2ovgdPtyA56kI3fIB5FzFQQI;>YEyB?2WFX#mP$aU!Z`EmY7=XuxRe1$U{k>Ed% z{cxs(g!3G@y!coGR(;Zd1TGP2%cMShwF%bB=7&H zu_?WXg#B~7)B9i-O)bzilkhTAokgHIekk?R)kke8m_IV9DkvfO#h)o!ST^NIH z7$bEI7-1gioidN-p?`wEfS332TE55EVLfska3j|NJ8~WHBiF%4)-}S04~TU=FxK^;Sl5GNT@Q(MJv7$!uvpi_V_lDkbsgSU z0*7$Fu0kUBmg=siMOq$z1env)hJJWoPxLQNv~l92n5^N{P1 z;~pgB^uM2_Ay*+sAvYl>Ar~PBA@&e+h&99*Vhb^aKK$={(zDirgm;1elLWtkbBBW7 zxe-{m&g4ZsMM{y1qy?EmW|No58>FJ(yO3UFB3VkdlB?ub5)!kDJR;E&l!=rfl}T&T zgG?j`$SHE2e1S+d$@@|A;!55VB%0(QgGl{W-ucNxlCrh;by9}(B?HLCHa=sLd*mN7 zu&un1apV{B4@uh2`zNVSI*>u+7)h)^rATd3Sb<8AX=E9BnH(Zl$xU)s@e(UuUXoqT zI+77&FF8%lk&7gy1CfrDAiWeazao|*6(6ZI-MbX&M8?j*mwZb6k2g`JXFFS@ z2^mh-kj``TH`zjVWYs+Hdt^UZFyA}C0-trr8FH2kTcQU@=B0Xv93tP52Fv{a zaik;ZK~9s~E{FF3VC{!+LL0WJZVX8u6Ewm$_aUe zyh)Cc56M~bi2O}btkYK{J1IwMk%nXrSxsId*`*?lLmJLNHBy~~wH4OFxY9VC%p`ls zDH4?$f3k(dCELh$5|6|u3CJ$8o9rQb$v(25WFiSkB9fToAW6s*Bq>Qol9Lo9B}qk6 zlQg6Q=}0<}&ZG!F5`>rl(cb*O3NI!&zWz%$f2 zvKP3)o#-*KK0y?`!zK>(^t&GCCGc*H*D~hW_t$oO=IzeKHn)<5+p<6U$fX zSo-0)6yXz|PvN>=6F)<3`#;a^d87X4vp*V9)k!f@^zj(3gA{)}mUujtd_0zVJeGbu zmU%puH3pk-TMlEui(rH?A^|U42fT2`!nq1(DV(3k{WzSB(2t=PL*Iv9kA44*oXyCY zjGV>D8H}90$eD|rwa6KZoUO>2ikzj$8H${p$eD@g9mFH}j+~Ql76N9thlerD|NGt; zdOq}e=<(3op{GMHhaL{S8+tbMYUt6>o1rH|FNPiry%%~e^jhe#&|9IWzBHD0pAGMQ zJ?7Jg+aKn4uo8HNhhA7=t&{hx*!SjJ-uoWVD@T^;yU=^r^<3z+&|{&uGQ%t`j8Nw= zhB}8a)H#f0NvL6{a~O+09z)MWY8%FgggS@ofEUJq7sh}W#()=e0$#uhc?noy4F19x z^uieQ!Wgi^7_h<^u)-Ly!WeJ@UgSE08^#Fs3$CgD=R2%{v96oCu0tA=@SVyAKrfR` zWHZ@9wvz2+C)rKH`^bKBkQ^d!lEdU3@-BIgoFL(yoFN~QkI2X5Q*xesNiLGF z$rW;yd_%608{`)Gj(kskAU~3y$Zhg7xl8Vm`{agRn=OzKL+}yaq5dL)M_3EC!RPwN z^9RP@Kj?-1AVELqg&YLlK_|o=-pj&Tn1>iceqMV#4|xiC2zn8JK|idA`PRo{$Yrn( zy5TzPg?Ph$zz*?+>k#WP5_pGsh&Aj*umW~i3s_-4%)@oa;r7S#$a-Xc+Ip}L^P41s z7uo;OTBu*Zis;>T9rnW*$wA;2I7ik)t%82w8ZaX3Kf8{^@ZYxoJrA~le^?I~p`QYe zkduh-Fa|##lF)l#1!4IZ`FF0Sqc?`-yOl;gi9_Vn)K*q)e)(^ngv4(sYXD~m2 z!574Wc?a=8pF+TVBN12Q_Uk$X4A&fBxSj#S^$ZxUXTWeh1BUAvFkH`o;d%y)UC%HX zbWFy4LmK8EWGn{ASR9bCSRiBZK*nN%jKu}t{0I;WWXQv0EG{q>7i26hx`n}MBM=Z^ zZ53D!K_9_#MfX7LiU8MK6yk4dfX+z9o-2S)OolkB2;hT+?q%tbBVM%L1lKrN2ZQ^a zjYzLU_d#$EMQ4tF=c9WYxQ_z=5Z`5m;T;IjAr>$yy3Fc`bxekMm<%y788z*1GWdn~;13x31*;D*YWi{> z)B|k59vB1E8DyvjmIe%cjOBrhrD1kh8l=N9z-&Rr(lA>rjfou{qm_J220z%C0DDZ< zB$J`-Sh@$2q0gb6(&(a_U91n;M{QwO8fYk*U@?p$DJ}~41LtB8MZop74V6Z=i z0QSIO4-EFeU=IxTm|lczKnI34@F9RbFpMvE1S>K?8x;_sZQu{a3Ft6CpbX?gIp`lW zoR`33#~AmPe6xr7_jIHi>)x6 zw;&zPS-{|jpG?O50%N{G#{7ef#Q+(L12Ps1WGo)YSWJ+yxZoR$3o_(EeL%+I0%LJO z#^QpE#RVCQ3o;fLWGpVoSX}Uj#RVCQ3&$142OKvwD)gHM!L{Jm@qlp!=cl6xdnr&F z0w>B7D?#Ih5**2T zQHyjy-EZhax*h_?caThu2Bedt2k97~9LT_BzX9G~1GKE54KV=H;61nY72HD%xW9sK z#N<##Iso2-gE*XjAw&H!8*&-mr^C|8WvB<1PA)@zv2=17>W-z8%TQM={jX&5`u{F> zpz`FveDRR^u@$$^kqJ30O`P>iz0wMFr)*6 zJuu{pAwW7X=;A9Zff&jGL%QUO4h*{7iVh6rfx#~@mJjU(9oj380CvET4-EOhkgtFM z;}sa<0)ss;_yty6VJHtelm~|LkO$>}p&T%j0|t9wum=WvNCSIdum=WvU<_anGT194 zfITqS1A{#<*Z_k)FxVeP0DEA_2ZnrL$Ok|^F!%!of52Ewum>H$kKl$Nu)>fBIv$-Qs4-EAK8S8&22We0a7|H>Y+k%b(`WzcW zAY#vk-Olmmt`z+evyw!mP=jR1DQU zKql`q(6K%O8IC{X1A`x6$Op#q!4ClJFrUEK*n)I51Sk&-<$<9*FxUeB)p!9r@^Jw= zHv*Kyjz2Ky*tG~4bYTRr2L}Dm`U&!XZ_n@blSMXK2uBe>#?EOVW9Kxuj=`J)hVpRz z0v#B1xPE~S3_8>ibYRe-j-UgB4*dl>)^E^{pkw_B{RleNpD;#22Zr)+odq2j^ABag zKllWOvcOm$!Fd4c0}OtEp*--vdi$Us{;U2WA5$=vu=^HBhjPHsufSNp{+U0l9CnW2 zME2?k7;_^T%3tD$b7rUng8%zd2m<%>>|A5tpT*j^mP&Z5l zKiD~+Tn3+5I`{+``VH{AjKzXjKL2k17kA81sU?8EGA<%;2$fC#lmbr zhk1tSm<&30JtCK(U679X`!gA{fpSnc?7R=@SY5F^U;uWH0{Pqs;GbNEvXBn-#nM5> z(lLKv12Uvzbp(5mp>N33L54Yk`N8G|*kj`#WNZwBj2%Zz#^Qnw{(%8l{XxcTfH4^x zOOTKCGssvvHU_ZciH&oxf$J;g2kU>#4(lJRo?wUBgG`=IUKZ;g@DIMQ`a>B^hIxU- zh2sTw~PHtV39jumNEs0v!T+9R`P9ky)-c>^s9F`Og@O2aMGn>IU_O`a`{; z{!qWas?%_a+$`!~RSI5}W!kpt!hIpCZ?4wx?h{uRJD1iwFh9rv=SOnDwVE7o z4@M4{JLG`tFgf5nLk>7EkOR&G%xetKjL=Nx`#|Os1 zpUH4+v2=17jy;x6E@NphH_#&>9Lxc1jPFG|a0Edc0iK)aAm}3KA?PC*ARI$5L@+`yMu77P%wssGpvTqA=O_!r1O!V2 zD+FrBSfH+V)4fcIL85QY)p{U~^sqZt9-i^xHM_d?*E zuR?@kgc5`@1bEK_-ix?_a1#OE1F1o%L-_ydoj7gd~Ks2w5nt9if{Ny?2W67TKmD))hqO^5cGV}1<9*H zM!W{`9E$6WWFG_=)BcF5QF;*KV1!VFLWFz-TU4h4#L)=u=%f&jI2$1cAr;{^ihl>8 z34sRr*nuF2e99yIv*$S)R)5zy>3^y|1XLeOgn#z=<$pta{!!=3{|#;VN1pqkJ=nO1 z>l$`F`~5oh|8x!eeJsIs>c6U|8_I{d{P$d+{uTQi&Nsi`Q(%4nSN9VC)iL{0^-@OoN8iW&PalJSx97iipZ)LS^Z)Mm0XFCV zJ>LFzjyC}c^qUsl+a|6;_wESr&75R}a|kI2=MmsLVrd8$5H2F5BV-_4LdZmbZ|r0v zfZb(;T!cJ?e1rmoLIik6buByk-G&(Zj#dSd;rnEj2vrF1-KrV{_=Z^>0=xrTkI;b7 zh;SR>4nh+`Gs0bjdkEOKxZ03>AAu6Z0l(l6d_g{xgYy4w=f98VzsK$W-ud|g&Cx-G zzxVg<|9U$r^t&BF5J3n*7(oO<6hRC@96=I63gIBaAp{u&Sp+!*c?1OnMFb@TWrV{B zDhTQbnh075r}ELa8xR{H7$S@zTt(wv|F*nbrl-t~sqi3ku+AHf`<1mP&E zM>%3x(}gv6SUZRHa99I|^>0`^hxKb%1BP{GSg(b(WLQInb#Yj`hV^M!1BZ2KSUZRH zaagB^b$M88hqZNB6NhzgSlf0%s71&|a7Do4!a6j>`+H;k9tUE8e`o{d2kJ{+SFD~; z7V3tTf%^PeUC7)1U$y(s>O$ULXzTBN4DJ21e*Q=FG1jMlbu9l8F<>sk8ayysep_De z0)};XV4)R;^?1->O&%E5<$+;s9vIf=fnkjvmm`^Bzk4xRn@m1RCc|?9%m#iVV=_D^ zz+`w{fXVRO0F&YQ0Vcz91WbnK378De6+nh(o8TXwZ$dvoTwv%Y@B?=LQ}_4K*U%2= zH(;n2)B_j*eFcC#s2kK1Y%vB~(4h?=!!Zg#?NUd8=Lp!d1o-~cG4xQS13AU^8pC%- zwxJFhK85-PaW2yFHt6{!T7*hNizS-KX)JnBdj=h(Ye;89x;%Oy0^g1*ibeb7QAbOl zgTsg(uAD>u^{r5FYNc>f^}q8M2E zG1M-Q&!cuup?V_m?^=(3xr%=IS(E%qpOfn_Pm$YYpz1FlAJnbOYfrHLhpLWSjUY+0 zPRrF3PjqqhvbDoo6FdocPglH+y^|B((#yt%=n-T?a3T>sJ-mo6UQSK{HUxVoqBYp~ zc-VUq@jiC0PQ(?{>V1qjR^*cfffAt?>D`;@m)9Ew*8X}{$hMJw`B@e-GB|*2@OT?% z6yC$b)q@5ncd54y=i#r&G(6mVHj zB8~}rE8yC+Y@P3vDBz?MEk+FAD9Fa%^kuusq^LbL)IYLKT`{Rob4}NDsNz?(H?5Ss zHx(-~p86^Hjw>coa`AA-;FUyH30BRP=_yf${~U3DdP?a8!_EWmIBzTM8hE?#T3}Xb zJT^Q)kSTSzK1%yjgu#IsLve`*41n@~Xfm;}^SsDxcJB^&rR#9UjK} zb-OC}1_RgTK4Ekb$|d20eyO|mL(aE6zwlG?Jf1V%?y z<0T(Z2b*nIyI>uBZ{zl(YPWw3diM|_)cAwWF&?h1RXgM|=;Tx}t=4&;F*@oEhq~PN ztqx;bjn#{!wTA?>6Vx|rju;=ZzN>!BBIA>0(7d{HHI+;Divt>pGfg8SV^$hW*Jjwv zMp8Alr@Q5Ov2|%A5^SREJyvOcnU@(`bwpg#V=jL9uBD6S;YEp!bw6@6xB9m|+E+iI znVBl7>{+@&%Q|zUq%B5XOS*Nizn0HOYrVv|So?)itpbI?ZzouXv=Wnhy>c!yAGukh zx;aKePaI)R+f>l8z4FNMrM~XQoDWCR8Y70Ply+%{9N>Ju=e)kQ`Qr4^ zsqd$?w`Yi+WqNsA+ftEg?gG;n?EwANl@!Ohk2>o&Z+hcvakPzF*kJ4VH} zx_{K^j$u13Gft=V6JJBzc3~ZfE49uZ?DjgXM*G&$IcDmxJ*gp%FbH=QfcwYu@d((CJ>?hdzB({;5BdV4e^ZpPRh*5k{J z%DEyRq!-kjyr)d{x}M4GyvvpsZ}mv-)u(ryWz~PLe1m!B`BD9xEfz1PbRzX1?C^bD z@~%!_X7hgQcDGOZ8=r_s|LoXfz@Qb@AuW5{pq;)ab1!wG!O!HD!~AOZ45THijc%nc z7}#)ZuKtqEe=PJP%U;q0>tnCqXH3!kNIMo=-t=YPq3&Z-rb46&O)5i25%g4&AYph< z9ye=W<7!y9FI0T!@nu79;jETtZBGnE|N#=#tcV%wRjj4$yXUv2qp z!uaHOLxmt}_T!hO4kV|aHaH$jBUPio8+-ix+q=4I>35Dx<162rtonLfvv8PQIDEee zziQ{XEJ1=v;B&b;m5_5L$8{LSx4n8`;zv^Nacjq!9@BogM)Rkzsq@G=mDkb^rgyKH z@|N0UnR;!zDSj-p*OY-Uw8lAWomoksCWj)GtXcna3ts)zo@N~kHNM4bip)Zey<^r_ zd1ZDrMVn9L%x3fBw?_OTqD9a7=Pv+|!Uk$!9|5(+obgYlf zVpbWfW9;=7l;@4Yy+uA-G=F#9)7!L{FuT~!cZy^}@C}lw z7imi*^qa)uLCEW(ASjaCpXdvT*nJJ>H;hU2Hqb-#35 z=Di;mD->L9)s-`&~| zjHiN_qP6c0ejP?7KWl^6w``13u3C$(7k&M$e%M+eUg3#X*%l%rNl)wX6D{II>nwfv z;V|O+c>yNk)@mZbH`usg=Oj^@Hh!-EF}uzB&g+#O(Z_7!PneG^Ih?VXzg9Q-%CgBu zJi}r9{Nh)eNX6BoLA5-#y=uF!#?2CJPq9rHUw2Qj{lHQ1X~)J6+s!`I#~w#h*bO8< z;GzB?Vz)u^hu8z6qn(Q4Ps>X?v+a!g>K7wy`|Qfq%KUBb(AZzqDlL6oEo*OVIWVy0 zou~aL$%h+W;fwA26|?MO^j_PqH)xZ;V$bLh@96({Yq+WdX^oKzel)~^I#-NWc$ZxM{a9Zvv$fEM-gdS z(corIr<3dBnoTQAogTG{U#Lw;a%!1xksT9mbrP$dOV~~M!)Z6^qNKqtLFW%g2y>1t~D<49=t+3iltoAY`?$KQFC_*Ik9;DU3#9& zE5bt1E~@7)JVCc6lZWVCz0)1JCATZNCWo!LNp0!xD$T&*t`v05wS(FDVM_XlYlul( z=&k0hZe0>V-TK?K-4vqJ6+hs^-Smo^nfC6haT`k(zUWGta@#bByR)Zox4QwOoWOx3 zLw84`#2>Ylaqhzd{x5|Eo84cW*~;AX^P9Wwr_75UKY2ZRzba&}ZM5__HJg0uzRP)! z5`l#e0csCDXw3sjXS66u5513f^ACxVPUv}1)n9NTb$hn5SH8|BQR(=V3>)^7BJ+=> zuujo>GPZpkboY|;+~V)4-`V5kc`5bL_=&?Mo>ogug5~Sqcphvdg$y2H@}j+}{i^q} znwPC}5-l-5)ay32kJitw3a?KO7>9nW8u#k|sZkazkN1u}lx4U>SI_%^R@>yL?J4g% zg1^J7JB{8v(ekX%wcv^7G|4s}lS}V} zWmSLr%s0{RYWpbYTRo$|Uy83L9rD}P`kfT=4B0$% z&`%(%@PNh!4?pR)B&p1Vd_PTpADUL-7k+M~r)|DxZSs#j#v3ZNPTAkJOx)shOn^Vp zX?LKgSh;`5VV><&5u^TQM&EWRz1XmjUbyIkBVxg;&f<#I2c3h5 zj?1bYDb5Lg=F!bMtTzyx5KlF-oqIz_?bDMkMztjH_J%C>N#o^HpAzAdp(*nIT7m7>x?I>sAuHkOux^b^@)4%2XeewTq%l~~* z{#C8mnCX{ajl4mDe!in0RhW(@Ep9WJj{aAw<)tMoAN?6(5mOXKcsGIv$=<~lPe5)+o_~_=f_JraK=t^OEIi561C{-=9B+b?7ZGa! z-a^vi51GdT@9KfKF#XR-L4PFKTcfV|TbbXyv0K=?pd@>1JdtEYa3f-xiH;Vi>HUH6W+>>;6Xs0?Sbbq@#05+ zc!30z_u*|^yWek>(;tOBh*n-`OrSyOWbbT`)<=A; zh(v3mHH;hhBa6n+I>f3gV+)pMW?1N#-)6pr(!uT~`Ujg1l}N5afaz+D4w#3%8w`1_ zeRz8k-kRu!;-LmuOX0mpL~ME_HJ7+jO*i0`H?-zx@KUf=lliMbGPz!hbV#d^V#L)U zzJ*Ya0CEF@*UBGDPZuv64@~=m;eB$sjyW_>DNJy3)ALA3X5beP6cQE@6%&^rSXx;V zZGzbU!&21o03J!x-#0Wm=_Au(Yw5iwCQF)?v532}aL0dYZbA#q`G5phv*F>!Hm z2?%p%DLQmw)E(pBm<$>imz^{8PjH|JV7y zLI?dTwBesRg8mgc=-+MlyHEB1)UotWb^b4Mka2RiV;|vY+c=KAxYh8z+-J$3u5NSs z`}cdw*NJ|U?kdhK9n*^?B$oTI#jf(&5O%JWg~MU0=1WH39#_Sl?qUCVwlYOWF4Ags zpL-5rmYow91t(|}=52?y`n;7m7Af90$rL<1`%NyL{dBLvM?cD(8M;k(e7$rO9Qzdw zgk$BDQj3ax9Zzkh30vRqC^wa8(4gCA(Bhcdu{*xKWs{@m#K*$Z6IGE7jXV-EtREOOd<$56WH>ME^y?bkj%zYcJH9--&u$XWQD*qwZYe#UPWHy5hI468MV-VF zvv)SttfQvi&AYf1uXs4(=IH`KuJts%CG+!3H<;4i4mll_5sY7dpIPdANJ81p?lZ$l zwjoaoq_3zgX3nN^Dwe1F$i3>?^e~NWp;aJ6;M`zX-=+Zt16f&}@2nwHhk{Of>~XXi z7vWN|ks2{8iDND6np}+|DhNK4Rm%`HxNi02q{CX-L%j_Nc3SL1UPlf^CFW4Q&sX#@ zrW`6Z)*aqZB-*|@r<;k%J?w(M0GG+p$EyxSJy1Muydg2PZ%5iwhVZ7yGVP3KXye@_O|uvm!Wtc7w-A~}n{i9n=53%y=5B{f zN{Q@Ht-BxYEbU~~iYiq+tPph{Z+X+^`V!}5_M!o%t&C%fg|(Kr%%xB2H}B_{#h1Eg z6jexDG|Tw7vIo+q-C*gw`*@e8cmX9|@<>EkHzJe+G8en`>>~vzj$!RuZYP^*__48`}Dc&r29LzPMmtU zKr3p>!1!@NwJN}7r|m@Od}sJ9vr(ZR>*zMwccGkbs!sM8UJ+Z@ArMa=op7ys;`*bt zQ)v;jGC!{~6<54?Td*_oz5~v-v$Ouyh2A%3UndsW*2`Pq_xUNQ+cbG zul+;~7Bxka>6=F6zc74fGD^L>-~Ww5P4efAv)5Syj=LT*K0e{}U?%;%e0tX;Pul7N z0S`{S290yMqL0k3Y*waAPHPZ-bIxabRCvVK zv{aVOH8L*YMgjUmT%&tEZwC(^d)IYp^uV+6Z9+#*(db_9NY*^QTtL&JS;rxmp;ERNp_YANiU-Z6S2VbH2u!h+uoD=;&V9bMOK*p_pE{G35)Hj^Tj+PrxZPIX55Wro|Uk?Sk)(HTH4pi ze$R`GRiIV!NRghQtK9e{}DzR z7V_n(4R5w(;RD;AfFB&qRh}J)-@}-lNlu)63ASHf2`alkbquZ4eSY}~Yc2Y}w;Ins?263F>A`$zP!dNfVz=qpT2cSrF>sG zi$wTHZFbgq(?wSu-=*LBHW9%;@1_5cOSVx zxHKlPvw3#@M9a(GRPSM59`j@;9Yd9tEbIDb4Dq!m8o~V{2m^k8JvKBCz68i8WDB=0K6lnjC$PoQs6W z{`#LkHwNg^v_*JDDN+2`94TsUKE~s@rv9DISc=^D)B-B8s@>w7RCuxqE`L8N74qmF zRUS_ylkvB^DsQDXM@*euWPkUZq?vl*%QN#0hBhUaYd^g1>9#m@=f5fZ zYgf;o5!l%>nmpE3x1{41qN?ok>Br-XbU!{3`Z9&`r#{AvSN?1}sT=50`=Ir9yyF;O zp7qZFE4%G9_Vx4ENG+HBJAX*?9@=#@pTL~1MQxJ5KD%K}^%?t7x`pk{mNYY6M^s%@ z_I_%9d3J7SW}#!M`SSBq##P>ujwa8YQ(0eo87uEgnIkf;ixkp1)v()e^QZGS>b`~; z#n|4?-#^L2XZSHF%fWMsrAk&@<$3Z8)?*b~xlM&121m4&w=vXi9&sXAYR)9tFzpx} z$Vz$7!qzrli@)Y$@lbFolEW;KkAdxlCu1i~b?8!YVcE@_wFVrUlhRIXcx%zv$FOzx zu)WjA^Iw{;@u}D(hfBK3Ui-y-q95v+jc-8r{`s0qD$qIQ~(ZtCLt@BNXIE(V7{gXQd$A9j#WS1sI zRqu0mu!t|Ma{B(jFOaIY|T=#o|Tx4m80UNo(Jpu>Mh*kb9{{hQO2gJ*A?`feNe zNlsjb=lf8pLa#5rwC?st(S2bB7c(|!6o;!PtM>^Mh0j(5q zh{D%CX0@f8EXtaExiZWt6ys@1T5*k!I@Pr$&U9T|JYjvi|Bd^B$R@LqXt(B*{y9O@ z8pc|6oFs2k{k^q`u@PD+u5D+V6V*h_d6Awxo$@2k&#^@R@)0&EVGA{lUIQ zqElO2yvG+~j4SmIKG*tiY?Wh{598>%azd*`?H+TPEMdia9QqJ8yPb!9#N{++t0F`V9J@k57q+j4xgle%6!wwq!f z?QrCJ@7touyNFk5JSwy1J1-W-pLJ8Lu|IVD%*GMwxu&KhQg!UPcT^j%^E0r~R@T%= zsm%D)Cp4+Jn(hDGQqEPVwr#p^qGDznt!U*H@g0IXgLo%euAysh&NN#+YuRJ2dpddd zTB9=?%fyX?Rnm`%y)X5fP`P^T=#!S|4C63fMfNSJoZFwjAjZu(DG7|sIWo~T&P`LF zUNYQn-F_)<`@Ls9lcKyar7{QDrGu3{hI|sEio@s12I%S>I@q3UJU(8VJg!sM5jj@T zl*w>-EH!UTB9C)Cq>#h%#^Hg~-7*`kqvOg)GJDD-v$k#snTR`eh-rRu@^V-y2Nl0v zm+sd6amH1}9?kxVKzx@Q{y3XeU`Y;ZrYKCm?w2Z z{Vp4;xHHPL?rzMua;4MZcnnRCwHMXPHZL7TW8;%bpZU(LX-JA-T4b>8o1Px(pOoHm zw=#hAG2PK3BBftbSzlv^SFUk4*WJkKy$3bkoG*SwOwx3x68chT!>^wCRJyMuB%r%x zNG5Hc?9aGWh03ET<`=?!pT{=}b=P}P>7>34{B}FfpocUitLPjbVRI;@>_RH1w^iY} zLbY7K&A4}iMt6eCbpsjdbWArGZnDSqBxK_!&;m5u60T$A$rS@~*r8gOI&ffb!d70=jj(>tM zNg~;&ac~rm)pYkgq8%NK^674F$BR4ypLO6+RlDgQ0KYPd19c`Eh@QdJXGL2quDoe z`J6M9nniwLd$JEnT@d;5t>*#1Le^%(5tnrv9^k@0-;Db4T{n7*OU8p3mEhNjRgXL` z1qthSS?8GfJj*cj-m`|GCF2D5s-J42=Z@Z+e0(J;m)gG|-T9#i}To`Jcrc z?X*iDuBr@3Fy-0De{1ObSl{{c?ep!L(Z(AUNjJ~uRfx)&yPe|aKQ|XmV<+&vlMuei zr=vtVuvT>chNd64IIqn-QkC~A1=LD zpv&eV^dhv2Za;DBkReOeqrzM*+0BIJ$5OA3oH_5p5H_|~fVz~`*qUbGvYRye=hwe( z(jCA3LC?hCPbg0Ad^uWBqR7I@6)X#FIJv52VF4%C0s^IMI!>;CS<-IC$-P~cIA(Ej zUqD(Fnv#l>i?|LV{#FA{?%cA-GOKuR&+w!(1)a1G5VY(z8 zewH)7y%W>J*Co&SO30j8(Pc+zYeWau#%HhS+ApJaW=pWUm}e@NESUqMa}y`^()@Gy-~$Do@WmL;nG?x%c4ugLDr$ zX7+nOiN>`#EZWmV=jq}mrz+2GEW!CJ`BQBekFyIME6Ek_=?i_*pQ@CQ>mfJo{=vrT za)kEb$dLWd@(&+%(#n@TxW0hnsjYcsLP8pG?NFDtdRMn|m)y7EirJ-1#g8W#;!m8+ z-F3=;M=}$L$vpCw0~Gzmp&3)t?5$sdYpQxAHRWUaUWe~5`!IL?WdSkj z)axFfi1k8J8fPya_z;~rtKVVOm*2Idy3@C5agSrGyJO?Q(+SNXteng<{!>a4TFO%~ z29nRZ+@E|rUwX5Bcsj=G{6t%~LY&rEJ(ZAI1^kKZv@rnnd0dTya6$u{0V zH5%0&~gDEL36Q{G>EUJRf z^Uv$<;Hlxj)qcV5Ou-qT_V zzD|PnA}%TO0TNI7YD%f}Qr3nvH>oaudlnV%|88K-SipTVPoWn-p1rR~E1izK?EX=@ zjw{Rl)~4O;w+BiK*LT`&7#F0zyT8AL``U&lJqZ-e3Ar*fU!D_7w#8AmhAq7Id)8vR z*H1^rAiMlQb*{M4mc(ibw_|&|O4gbEc>6HY)$Hb8N{x@c1lks3vumLhtB(s9o;ae{ zFA?YS$?HYduGVttD+DWhjW+i_=d+vdkBRSixK;AjDs#zBHC123#oet$*({Y3s(_K( zoxJKS8Q$qKT-F;tSYBXrm!D@dy_+0%M6kX)PYOreaMp+D`NhwAICw%a{sG?neVs$2 zU0VANt**^6X~FNkha8lqni_-icQrrxD03-?@!~z^k{eSy=*MTY#=7o}PJ~KLs&$`Z zQ1dW+^eMY;Cl}#Ck*Q+c`4@3llw3}8@Aa5J$mqp1UVm0_+rT>BFNW{7=kwTWCK;rg zyY1b3(1`C%PM1&f>q@6EpY_)c#JKvW#Wd^Brj8u8K1Y|n=uA5yBX;3t>Z`a?v-(qV z^&E%a(cQxh2cN%wtdeIUapI%l8{MzBFPcfe`Zz3RTWP!RfYWT6;jZJ6F`knraXqj7 z>Z9CmUm5#kdu3Dc<F2R`!1^sw!|&g3~yb`W0|fT`E(-qwuMZ5aQhKepQ#IJ zmz{Gmrp4)m%o>JS^-Cy1*RN~8$Fufv= zl-erUxur#lJiHA{ed?2DsiINs$|ZmK&O#Z}iH6RK)0=mWUVarj7JC11?O8F3a#1HC z*A|xP-EZoi7FLJLnJON}(N?ofI6dX2fBbed(UZyO_J;hV%_FVb#uX9{Jhv+oOKpqt z*)r&tJ)&2!xT!f+siAgXvYFnK`)m$x{QGBq_B-#JWvdhByreS}yWYmoJbxr@%g7i- zxzf9B*WyF?zIB=2HZ!%BXJg6U^@6l>VOGJ*&AT>O)#D1)xl@I@tbS^$YaHxfKHvCE zX`v*^&Wa`Dc@)*E{-h-Bfw}%l`Gcn6Jw-}Q_bwh!iaZ{9y-2AxY4>Sb!K#y=MeRNp z>|h+Q^C@+TTe9ALRgrS)(cY))70wN;72LVg`=bLvB&>Svd)4~4iXrdv9~a)Mq0G%+ zXPNGVXEx7Hpx!KIx9TBXtbcP<;d4=I<55TUUF@N!&htFeQ&nyHaqZ%Fwe5HR${kL`+)DwaE~JU^hFn4Mk;Ua zr40i^d%8A#Z=<5Nvrh_m*lP3AU+*Mlj=)B5}lG^P^#pDxPy9GF@a zxuj;eIN7l9q-%jrf3Y~+w$YTYws=y!D{NkuyY}4mtn&*d72M~-2%iHyjgtpoMN3O~ z61-|h<|i*~a-UCT`4D^fQ4PbGX>~D$!9y|sNrvhU_KK23$s06x^j}++7PGt!*`Li^ zxz0jUz%e!8SaQZIb8pr3VcP1f9}&UEAA_1cERL1Q#jPcUH*AhKWMsYZ*57(-PkCGX zo-0qTyBH0dXQkC=D!;6ox$-p5{DJGF6jf;d?upZSh8t9B0$%PJQ(KMi;3+6`xI%xS zV4K0!+>A9`OW)|9Qawu!J(|6Ldn7xR-_nKRD0ynr=^n+!1g`x`#~S6%73r#Zmab)% z6#4AM%zGed;qiNu#Pg>vM$1>N;|nys7#P0kS?x~|!a4gru@cXPn=a;^ORx_Pt65Os zn_A1zbe*!RZg!Q~i0yoG&=*DZnyZ8pUwoTtCtq)8dm+etsbG6;wjc}1p8Kh#_1VUY z5s5im4}QMrTICfV5afH6@|{Tg_3wvzn-z6P&rC|n2-heYr!oqx7aTT6Q%f>$^=ECm z;&`w7N43(H;?vZ}GgB{nTxj{KB+Ys3l3v;AG%K0>DEz?->XE)*hxAHCLk@Pu7jd=U z9xPz4*zRyFJd?J0=JDqr(|t!-vvU-ki3;zGeD79WO4)OxJa<&hI+Ky$mU5G*d~0&& z%MF>P(osf!>%X%v9s4j|p#Nyi&Gki3+M@=&mYC0ej_#A^_*Ag2vtdk6xA8^Qfe)v3 zoj-0`e}a#Ln``Rv=t7=zVVfOuJbi}2Hb1Z2_*+qm_CXXb^ZJSt76VCVZj=_CYkqO! zzLxH>LM)9RV!3qP>zk8=H;eV-!r z9r~nb`#rBcezJSW{j`CnmnCs^YlNy+xUB(i)K{lR40R1H;swtMM2gZmu8P)(0ehFZ zHIH;^8z=@uh2Es!*h{H&Joh9?n1!HBd3z$<;+j0Ohvwx33e))k%4yC#Uz5idJMkT} zauwgsteOz$Oc836r`)&Xu9NNMPGuV`cL-l^pY4|!`8a&6#S%yNNYjqz?mEL(JLJrn9vfg)pd@Y2H*XS>IjRuxgJa|IYI- zZe|U!_0xr0Us2@a2qwKOa?d@}GkU7$=ft>4nJK*iW88-F!{$6uhYI(2Zt+TXO#7rg zxYg$ymlnf;M+fd799~M%Iqh0};i;Qr{mFnu+82!n-<8tpQ_yR+I9VAlY1nv4y56);A*)0_~VSSTf?+!!{|ee|?&gJ!l|fy9=Oz${kbLM8gg+F9!u zHmSVap*fO>Kec~6Opr>sgyy5#SqsxjJIBhAT$u-e^1SV5q1P`D zDi-8E4ZOHmemRI@N;Z`_@AUgM)O>OoT9K0$=eTypX_e(rpY}_4|}xU!IqH+S*^Gzd~eb?4QX`OxrhA_&VG9L8;83OUE_(uZ=U@j}2+o{a}epKE3oJ zd1mU`m$g$G>fZ;SrdRHyk)AsjH*d{Ef3;qXPBFJYL&Ll7lUT#H-fhkqoAu8;GJmBz z{_&pku2Y1za31}+p^J{5Nfj4ID!v^2A?V{!`TS($rmE~FHg=)dTQW?mbCY(z?;BAQ z${UxtT0+MgmM_IzzB+!RY{BO!&GvnByAvK9XKTIAK!0><41dV;E01vf^%37!%zAqq zIf^tBx24NgPQ{hP#OZx&syx-b4d2{-xx^#oO8LH+_1z1M!;i`T!} zI(*rDlJtGwM$3It>GI~Y6!$)-=EMh7B-YNThHdt7m-jv((@wguOSH(1m1CfH!(-jW zjX&urio+hq?*6zY@4XeH>HSN^vTA}<;-a%EokM{}6#*@M-7gNUZc!+Ic4GSr*X(U` zG^>;2BCC=Ixdal&Msw*e?hRwzTJbsXfk)66tDT*_ccw2}?Iavv{n7Q}^T7fNwa*i= z2W=&!tZq>}V2Pxbb2-2HNyO__s~h@G-pVLYd!$;wMXTV6VkED6&i;s_8A|CBmv%kc z*#B;BP)TY==VZy7v#V~(-ycn=d%!|-?VTm_vuXv)vr;>~9HcYWr)Mu-q<_~d=}TkF zaBuxk6opa{!6D%uM~qGA{zcAbV{$1k=EEz#e&h|c_&Q&H)VilmS3Lg>$zCFBj^t~0 zJ=jWLczErZ_iuJv2dL0KH#fWGK=DHLn&mSd=?mLB6Tf*m-lNbpd9g_E+aa4R=Uuzc zYWtfTHK#IY+C$o?#)|gMN?AU|bCoJStj}CfveFa!^q`h*pWwhb*|NiaEP+M0`x|-W7qn*^3zC-}lhRi;f(<4q02 zN`jZ}>HZLC+w(=Rd&ZM5SKln8sxa_St?PA}yZj824?l>QkM?Zu3|c4!`zgOr_E`9iQ|pb$b21!*EOc z`d5@X{yOK{J}9i;5h_Oaqbnus?UsVt+P8Y~dn{Hz{t*_)fq$hvW{>Bd;j3;r9NhDe z*)ygmQ4T#AVH0|x!FKor>ruYLl{#Z{Wg{}1Uyr6{TaC^?8P4DB^K_7Ii(&kdqS=9T z+c*{P@q+PnbjL_C{UTCf6xW|^9z9Cz=;1lirM`-0cDpTN%1@a+xcf@IJiYw5xIB;eBpc7y?>7f74X(Q6pk%M@)rq56PZ( z9%z#W-i6>y#5pq(0M;|bBj9_{IXy%Ts#KePn|C&5UMV?$**R{#2# zP2i6h!DDZa%~QToY#dg3ch%Xny`M+>gO#sb{xSFMF|gxd`48GBWAFLP{qj~7H=>8L zJ!yIW2HHboPqaosUC{oWU%QNK&?bxjn1a5qVdsjruz0zk`l4+xmQF+$qNk6mha-6s znG=Ggs|VVzvC`L5WLY=6mqB5u0Af79AIXz|c28kF1zzxW1Q%DOmW{D~f}F0Oy87s-uaMI;f4j(D^M$IAsI=)dJwz;+?R-*)6INA+}d#XAu^ zY>DUqxO#b5!3R-Denelii^ubC<(AW|QHMZBT6wsVNW9iWZ+m3C+#=Ly4%>S>2IVJ&}?#mfaY2HCoL*!!dIT2A^S7Rk#B^%BX(%juU3 zJi!UI?bo0|+nIh%D*Uf6#^8>+Gvb)gY_UPS0r7gYU1}{Lr1|L4(u;SsNMKqwwxr?uMm0pi#T=!652F|X3KIUS3jaP z4mX10z<&ziPD#J~pT+E6X~!QQ$gxF}7)>D}8bJ2$UYKXRs-h&`(%y4#^sM}VdtQJbP|JpYC(RnZCtHLZbP__0DoXxA)M_T!k z!Xtzp1o#7G0FQsudy$UkawV;7f84i{jbkM-qTSZ$qiBe+G|WfeZ$A2wuI1&lyfIR4 z`Cu-KsPrH89Y8h&5{c-sd`A4$W0H6`5*lRqL(=$V-j8>-cQJSMCVJSRIfJ%Mq8+E) z$On7?55~)1l|T4Ld4lhsl&?bNfA7~Ps6NnNn-CZfKnB20R?xv7wtT|)g<}DI0WyFN z0rJ3hF9Ljc33jN$N07i4>I&OF!5^f7EdYEnBET`+jsR(3!;AoaAqL1`2QmO+XuB}t zED+og!Vu0PTt=uwXhY~j7)F>wpmJrz(IIdkNFb;n7$Ddq#3QsKyhiwju*!`Qw;q8R zVK0I-f;xf`f(?Q{!byY-gc5{0ghvQ(5k4WTMe~6XVJ8ANf(n6ThyQiN&bV z!qG(gEPbrKetjsak>S@d-Gkc2h45>n$Q6D-cPTNnZb_ySomL z;_UxDKKIP5#v#N=!X`inL~#ff+}%Bn6Nuol!QCkoYk~xKcXxM(K%qdPKud*|0>Aei zpzZTK=lY%Bxy~Qwx=z{`pX_F5Wp?h_nfdO0PtUZdsPx|bQ)2rB7o`hpZ`oBhhN)s~ zxoZZc>LWSnVO36hi+^J2I+{Z7%QU&ZkbN-I{`Z}J`*)?G%QXRgP7_mlvSOrA$@*yxNbpBIldAop5sKdIlRm1Pfb-qXW!7X_(ZfjB(1b3p^~=4DQ5aNr)mJUA ze!6ErQAe0GA=(MmLFNsjixzXCg#dXI_kIa1ezoB1soUo`&kJ>YA(W#IHu4 zVJE?`6`G&w&wtUXFqwtwpfORHp`@s)28w=~u3CgqyRE7`zA(vqRn11LDvAhmxa#Px zM+$Dv|G^eV)y4}HS~+gQQ_`Y%H=>QrG~pyn^1(H=L=`6yn5ghK2hBZpU5qV4D58+A84RMMv7^RPWmH6*E^q0pH&LAf8wpP>N!L^} zb4wTAszJ(SSU1e zKHgbP6gFvwT1F>1vw40k&2n~niWV#xeb$xM47rUC>9oaxiE=o zJf<#e)?lN`l4UWfS~zd}F>81qAIV!K`q5xmyfHs z8C?`dO;ZdNOcq_yf>lLSabsywop{YmB_B^$!c|X2v$(1zf1P2&%jzY}!dZA2go|Pl zZn=+RFjNi@na+x+#6T$@)Hm`WdB-e5G4(Rj>Va`|MVm~sQjFc?TW7VYO?Wf^v+1zn``ma`QAkw( ztMNqqqv;f*(Y=&2jwYkHy1KH`{nRl2rAbsZC~Cw1MPo3DO8;mGLYO}_1^u55fj1)e zop7c(RL@<=g8Mt@(!|IxAbb~<`ZZ=Kjymya{tYsZ@A4v(%<3B2i zRbE4PR_aQKa5gqkOpXS{$LOOpi7BKPN^@Z4E!QlcQ;{DCa&1}CsbohFtA`S;MBBVo zZ>6QE&FWEpEa|+{^7ExCt2kPs@Rc)xXT(4#s<-fFZu~2=VioR2Bg3=QkdMnF=WcSy z5C6ykit;_%YH~Yu0~@n)JNL`wom`e9!2C5f!~ZV_m%{&j{E5FWSFKjPM$KBGHb$EW z4z?Bz<>mjiIrWUqsPFL{w!yY2n{BXlsBLKbj=77$|HFUSfE#&;gw;x^p zC{=fkV$!84?Zd;vJH{n;;a~xqv2xgwVEIc?#%5uYHnnHBc)7J46T|$dJi0X^B(p}5N8CPd^vXdP#;16rvJMPsuC=N_cv z!Pu>jQATk;a-V1_DVN8sq}-R$ zj3Xiac+3PjcngP*x}~zKk;Ji(-2K&rHc$X{dv|a%3J;>_i?}6 zZ*g{IY+TNS+;{zQc@)3<_o3x44PlaSMOoHs0Y5p5Awnt`QQOQFcmL&!w31{3qSay0Bi_AAms6>LMV(N z6hScLL7rj=K`6oy4)zn2NGM9QQi7EGCfiG(&T=KufejYqUXIv_pGzKu56erF2FNViAXUBp?w<=z^~3 zhVJNrp6G?%NJby@MG8`phII5pe+zM#!S!Rw zaTt$QT%JJ6lXOlbCt(WiVk+8l{WP*YIh~w=nTX-?EHa+_ie&RsnL~CZ=aOGz0g}1A zkW3-v;fZuIlgz?mEWuJNLpJREeL1-TD>0JGYsj@&hjCn9Pj0|Q$U`8T$cf};atpR% z8@6Kx*Y6;AVh{G>2#(?C=R8iH#EH*&iadkUpYtqv4(D+J7jX%faRpa#4cBo4H*pKM zaR>KsA31n{hxitc@EG6W37+B^zQ=RCzz=wdSNIWc@YCn~nfwL6Vm5xme!Ru+_yh0o zC*I=&K0;xhu#d;#7S;>UAxFwH!UQwqNO@MU5v(|ZJwwGAa>P7WxFHYn!X5b_2UYik z7rfzv{P2Yz{80clEaG_tk_Ax+g%N}zV1HC8ied;sD8dkq2t=YdN}wd7kjPe01+pS4 zVIKEinM~sPs$?}(hn?$dkmFGklTZt_Q3rKV5B1Ri4bcdV(TC5^zDPkT`k_CD;|<1O z4aQ+D#$z2OU_B<`45r{Lrs5priL2zvv*by-4X-@4nUsZ zOP)k)2@XS^4Q zK=*JTIe37F_!f`w7~kOup5ht4$8)^E4|s`J_!;u`|Ax2t9Y5g@yu*78#s>_+M+}9+ z24NGhf2A~qie}Kz9BgqaEnt8=0ge&!WH=_Yh8gliJRCbw+QN!gFK;-2g<+`W#I*Rav^V&hY#e5hVnz6Y{(b# zghPJd$c*9-dGenEr~(_J5rC=)L^TvdbreDk6ox!8Q4ngO2x=o3@+3t?Q5VHf4L`D&4j2Ofs4)I7pB9hPrUC|BQ(E~lv3%!wyKIn@Sq#_OJ=!gCofPol< z!5D&}7>41nBLfpK5tA?(Q!o|NFdZ{66SFWIUttdB;%m&qd@R61EW$U)L>3lf36^3R zvauX1uoA1V8f&l?>#!ahuo0WE8C$Rw+prxwuoJtm8+))9`>-Dea2^M72#0Y5M{x|t zaRMiC3a4=fXK@Y}a1obq8CP%>*Ki#-a1*z18+ULQ_i!IMcz}oa7LV{4-{A?K;u*fj zbG*P0c!^hdjUVw7-r#5af?x3)-r{%sfp_>5@9_a2p-^w&xT&H-gAN0XFu@E9tZ)EF zIus{3!v(HzLmuRXJMzH;p74Sq9Ek=ISL~PMG%alD25P(A`Ib( zKyk#P1maK<@hF7^ltv=TAPHsB1?A8c<LwrhK zR6`1?BNa7}hMGu6E%ZZe^hX^GKwS((Jq$v93`PSCK|>5hBMd`h3`Y~#(G(eIh7o9v zk!XQYXo=Big)zvKJEmc@IZ}{|NtIkeYUENO+)d^u_mIBiUeb@;NBWcd$pYj7(ncO61IR;UAbFT9NFE^zkw?kGv6mkifN-ibS$Yo?YnN9X1my`X;732VNB{`5>MGhiYlY_}MZY4*P+sHBGc5*DagB(ZhB*&Ax$O+_b zaw55hoJ8&=CzJcgDdc`~DtUmMMjj-mlZVI|yh<)3uaVj0 zb#gg*gIq!0Bv+ER$W`QRay5B}TtnU^*OK?hb>w|=J()vpARmw$$%o`7@>_B<`H0*? zJ|?%4-;vwMC**eWDY=7uM(!lPCwGz0$=&1&au4|fxtDxN?jv83`^nejTl|RM@e}^Q z8@$8M_!Gb2J;qa0o`7k%z%o3YyhzR%Th`dLBL*6Gd$s96^d_XQHACgPRZ^@BbSp; z$Q9&MawYkUTt$9Qt|p(8YseSmTJi^S9r==6Prf2Ikgv&&9L35Q0#IAsi8iL~)cr zNt8lqltEdPLwQ7@0xF^sDx(UbQ5Drt9W_uBwNM*%P#5)39}UnDjnEiP&=k$k94*ii zt_pcn!Xf`SM|A%vkY!V!cB6hS#eqCAQt z3MEhhB~cNjPzj|`8D&rfWl;ywsEewohia&g>S%x(Xo#97Fg=IDr)=#19riWqc5 zEV?5OJrIwcNI)+nqBoL|j4l`lJH{ge6EFf3F%pw73X?G!Q!oZoF&1+%4PRqA=3xfr zV^hF#}5RX(OAPtE~M-uv>3;Lrg2A~@T zqB{nm2L_`jhM*UQqBn*i8N)FWqp++5j$`RJ8>Dia0R<@6?6DoQ{@N$4mA14_e)GBBYm%qRy7%EO8%IG_R?Q4vn41ZPx+3#z~s(QrdmH2lbF2_2G*K@IyoRqY(Gjp05nA)nxPnV#$p`C zV*(~(5+-8`reYeVV+Lko7G~os%)wlIjd_@l1z3nh_y(EC!eT7JQY=F@mSY80Vii_n z4c1~E)?))UViPuF3$|h#wqpl&Vi$H}5B6do_TvB!;t&qw2#(?yj^hMQ;uKEf49?;l z&f@|u;u0?73a;WBuHy!7;udb>4({R}?jr{e@DShPIbPrgyu?p@+ zp*HHEF6yB^8lWK>p)s1EDVm`LL@(Hpfe{#qQ5cOe7>jWjj|rHFNtlc&n2Kqbjv1JVS(uHlFb8w-HRfSH0+-Wf zQ3!<*gdzw=Q4~W6LJ@{=M4$vpq7+J_49cP$$|DLDP!W|-8C4LCs;GwQsDYZOh1#ft zx~PZxXn=-jgvMxsrf7!dXn~e!h1O_;wrGd;=zxysgwBXTEaDK41SBE}UCcO{ z6TQ$I$ry-17>pqpih-pR$wJoA&JLXO?DvHkg?=ivM0HY>`rbV7m*vuvE(Ll zA-S1s&hd^dstGI^ixPhCvh1c|WWVu5is-YmNqY!GKFlr(Q zwNM1L5sW%0in=IUn~CprD>7s0Rw_d4hVN zpq?kF2MX$Wf_k8!o+qdW3hH@+dZ3`5C#VMs>Un~CprD>7s0Rw_d4hVNpq?kFR|#rD zf|`|}E+nX132H-v+LfR_B&c5rYD9t>mY_}~sACCgMS@zEpk5@XX9;RXf|{0~ZX~E{ z32H}z+LoYxB&cr*YDj__m!OU$sB;NwNrGCJpq?bCcL{1rf_j&rt|X{=32IA%x|g87 zB&dA}YD|Lqm!QrhsDTM;O@ca@pxz{?g$Zg-f_j*s?j)#*32IM*x|pE;B&dxEYEXju zn4k_NsF4Y3QGyzqpdKZtmkDZ8f|{A2E+wd&32IY<+L@p}C8((gWe)mcE>iF{QZWx{ zn2&TUKtC)*e=NcPe1n0=#2{p02$ocMq(94VKqi$4aQ(C z#$p}DVLirU114Z2CSfxsV+*EWE2d%_reQm#V+UqnCuU(cW@8WRj=Xosz(I__A&kUf zjKUF&#!-yHF^t7=jKc|x$4N}UDNMv^Ou`vV##v0kIZVZQOv43C$3@J*CCtQS%)%AS z##MZUYnX%Un2Q_u8aFWyw=f^Ku>g0l5O=W%_wWtwBNI8u!UHVELoC6!Sc*qjhR4Xp zcUX=mSb?WliDy`a@39)su?8=&7C&GeUSd67VFO-cBYwmt{DdY>EVF2ezG#LNG)F30 zAPp^%j#lW0*65Em7=X4Ih;|r+_85!~7=n%%icT1Y&KQmu*b$2i#9;*DF%k(Fg+z=- zrWeZ*vTzWKaR^Ir7)x;k%WxFgIELjojukk8l{kr2IEB?XjWsxfwK$7)IEVE(j}5qh zjkt(SxP;BPj4il=t+55sw;3Kusi~7LrgKT~G&IQ5W4%58Y88J(Gb1R2))r5$!LN;Xo|jQh7>eM zDq0{7Es>5^=!e$mk2V;9wit+Z7=-p1j1Cxrju?tg7>3Rmj!f)E7WQB<_F@V4VJY@w z84e&D2eBN7umXp%5=XEKN3j~mum;Dm7ALR{C$S!nfr3y`2pS4QM-U7s0waQ9LQ$Ae3>JjIicmNp42}qg6C&V@NVuRl zTu}mUD2Y5Mg}f*Yca%Xsl!XV%!4u`-g(!HV0(?*r`B4eJs0=?;fj^>A099c_H3XnK z9DQ7K`{k+}lVkH%*}%4J$GkBjk7tf;Zvk7tkWgEMAu^_9zRa9bZyv+P(3QZ68@GE8aggAn|qgti^G5;67oKJ%zk3tgm$hi$ z*P=>_!t)xb+xVP|;dx~$c|2_e9BnZLUWLYl6pPFWNzTp;Rae-;{NLHal@Bpx7JbYt z^Iim$71r3Yg`5J)t#-~T&vS};;~!8zx>Qzk|JVmDUU!de$#ZGBa8GP2o=5A%i&?Fe zTLEo&9&LFZ?O!~4(BZ<<*p4>kWvAC-cxUEAOs}l`(Y>i!UU}e~eJ#sRxnDIfGAE$0^6Ww3 z$R7iOCj6KZq~vx>(p7`Wq?*kZ)oO51opo0_{^c>`^>wEQrl;!dUH&&$T2)nzY|aNr|L`FYyGYIhvA)YEK;FzqsCJgE&68g z#F<|$-n47XMx)76rc&irZ!TQa-Mz|`ZPj}Cs&(u4md?pDcKnn@x|6e8o}$G|Mb)fT zw_c;h35gRXO`f{#;Nc_3PG9S=ZF_!SliBLv?pdaM)QXiiZd%Gto4&$itx!3s+te9u z$({GVeAO;)=dPds{Mfw3y!qh~MS@!{%*gwyY4-9nXEROS zK7oZRSGoV>#jB4;kLk98g@b}imW!%WuR)XMEnBr|*P&BPd}30M)B!_>PguHY{e}Y< z)~)ZIeEjacnVkX$8#FykOVUI{xP645R@}u`FJ$pIgcxe*&c*DjjC7;YgUzKJ8fm2* zGs;>#tY)tYHOp)9W=o`pAwbJ-$RAWruWyLZttN{p+Ezq&w3N}J3_d2^(bS}FS$bNT z!p&A=kmhGr?H8K48qJn~aC0#apCDg%PfH^ntcH`f$!e@)E@DY{s8+d{v4X*BY)S`X zSIywi)~x!)`PVU9?b)3IYdTnsPI;q@R-?ObsP1XsSuvrxV;zgNR?Yl%%*~zZnyjtN zZ33OG##&ZCt#;kAnllevp8F2ft@dBNO%*i1mcpfk)5!Tr=??ZI6B@)jjf!;juuh$C z7*TuP&ZsfRMwB-d(>oZ0thKDchP)XYqU^`o8%l-N*UOvSq74ov%Yth~ZLz6Y?=woe z2tT8)nibDw;*2Dv-BJGQoX+s9_q z)<#!z$v3J=0i)4=HAHqp?GvW?=xRoE0k@SvmIKiZ;;cn>*62 zMuSTBLluoeZ)wP1O3iQ%)e{_BS?%k}`8kE^7L)31w9gxPQ*)y)v;CHl9XA(8y&Nw- z*c_^>&Xyt>ZOnf3b*`ZM^ETPv@ogmf*cpsQ)nqiAEN)g`2X9AjCs$`j7u{9MlP9mm zLwM?5!dvsP7PXtT(rt`(G4+-_~_}9d< z3rusgU-QXuHP|0mep;lJwPbW~x9>39@48pitj2QYT4qOMnuEXAPH$_mkM#1jdRQ9h z_6f$-OC3G+;#qpetsr`58b)lk6*KaO$+~@?mS1ym%An6~VLm5if56#W}`!wi_)y7yDNoi3w>!<6ZY)TLALw?m|Fe!(=m_f>Z zX#YB)oZ|DO{O!v$TRu;FeFB8EIC`qsea%e-p2p&049C7rvgkj zl((b>X_G&}#cdTU+WvYL8+J3=3V+K(uevYK%$>{fU3|$EvZu;x#4s1)kN`H^a+#E> z_P0Dc*^?vpXA(F3^84g}{66I$zfWcBaEF{qoyKn`G;Sp z@kut$y>r!jD=dx$jvEC1JXiVQWBFG%&T{NVp!V1K8dLz)?SP~(=B(9 Gng0SQorbai literal 0 HcmV?d00001 diff --git a/host-libs/js/shared/dist/wasm/node/truapi_server_bg.wasm.d.ts b/host-libs/js/shared/dist/wasm/node/truapi_server_bg.wasm.d.ts new file mode 100644 index 00000000..478d7602 --- /dev/null +++ b/host-libs/js/shared/dist/wasm/node/truapi_server_bg.wasm.d.ts @@ -0,0 +1,79 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export const ffi_truapi_server_rust_future_cancel_f32: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_f64: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_i16: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_i32: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_i64: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_i8: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_pointer: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_rust_buffer: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_u16: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_u32: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_u64: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_u8: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_void: (a: bigint) => void; +export const ffi_truapi_server_rust_future_complete_f32: (a: bigint, b: number) => number; +export const ffi_truapi_server_rust_future_complete_f64: (a: bigint, b: number) => number; +export const ffi_truapi_server_rust_future_complete_i16: (a: bigint, b: number) => number; +export const ffi_truapi_server_rust_future_complete_i32: (a: bigint, b: number) => number; +export const ffi_truapi_server_rust_future_complete_i64: (a: bigint, b: number) => bigint; +export const ffi_truapi_server_rust_future_complete_i8: (a: bigint, b: number) => number; +export const ffi_truapi_server_rust_future_complete_pointer: (a: bigint, b: number) => number; +export const ffi_truapi_server_rust_future_complete_rust_buffer: (a: number, b: bigint, c: number) => void; +export const ffi_truapi_server_rust_future_complete_u16: (a: bigint, b: number) => number; +export const ffi_truapi_server_rust_future_complete_u32: (a: bigint, b: number) => number; +export const ffi_truapi_server_rust_future_complete_u64: (a: bigint, b: number) => bigint; +export const ffi_truapi_server_rust_future_complete_u8: (a: bigint, b: number) => number; +export const ffi_truapi_server_rust_future_complete_void: (a: bigint, b: number) => void; +export const ffi_truapi_server_rust_future_free_f32: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_f64: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_i16: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_i32: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_i64: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_i8: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_pointer: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_rust_buffer: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_u16: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_u32: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_u64: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_u8: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_void: (a: bigint) => void; +export const ffi_truapi_server_rust_future_poll_f32: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_f64: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_i16: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_i32: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_i64: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_i8: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_pointer: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_rust_buffer: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_u16: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_u32: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_u64: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_u8: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_void: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rustbuffer_alloc: (a: number, b: bigint, c: number) => void; +export const ffi_truapi_server_rustbuffer_free: (a: number, b: number) => void; +export const ffi_truapi_server_rustbuffer_from_bytes: (a: number, b: number, c: number) => void; +export const ffi_truapi_server_rustbuffer_reserve: (a: number, b: number, c: bigint, d: number) => void; +export const ffi_truapi_server_uniffi_contract_version: () => number; +export const __wbg_wasmtruapicore_free: (a: number, b: number) => void; +export const setDebugEnabled: (a: number) => void; +export const wasmtruapicore_clearActiveSession: (a: number) => void; +export const wasmtruapicore_dispose: (a: number) => [number, number]; +export const wasmtruapicore_new: (a: any) => [number, number, number]; +export const wasmtruapicore_receiveFromProduct: (a: number, b: number, c: number) => any; +export const wasmtruapicore_setActiveSession: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number]; +export const wasm_bindgen__convert__closures_____invoke__h9f03fbb9b0a11e7f: (a: number, b: number, c: any) => [number, number]; +export const wasm_bindgen__convert__closures_____invoke__h9632106b2100de3d: (a: number, b: number, c: any, d: any) => void; +export const wasm_bindgen__convert__closures_____invoke__ha3786d1373b8eeae: (a: number, b: number, c: any) => void; +export const wasm_bindgen__convert__closures_____invoke__ha3786d1373b8eeae_2: (a: number, b: number, c: any) => void; +export const __wbindgen_malloc: (a: number, b: number) => number; +export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; +export const __wbindgen_exn_store: (a: number) => void; +export const __externref_table_alloc: () => number; +export const __wbindgen_externrefs: WebAssembly.Table; +export const __wbindgen_destroy_closure: (a: number, b: number) => void; +export const __externref_table_dealloc: (a: number) => void; +export const __wbindgen_start: () => void; diff --git a/host-libs/js/shared/dist/wasm/web/README.md b/host-libs/js/shared/dist/wasm/web/README.md new file mode 100644 index 00000000..4f30f9a0 --- /dev/null +++ b/host-libs/js/shared/dist/wasm/web/README.md @@ -0,0 +1,31 @@ +# truapi-server + +*Runtime core for TrUAPI: dispatcher, protocol frames, SCALE-coded wire envelope.* + +## What this crate is for + +`truapi-server` is the runtime that turns trait implementations of the +`truapi` API into a working host. It owns: + +- the [`ProtocolMessage`] wire envelope and SCALE codec +- the [`Dispatcher`] that routes incoming frames to per-method handlers +- the subscription lifecycle (start/receive/stop/interrupt) +- the [`Transport`] trait that platform-specific IPC backends implement +- the auto-generated dispatcher/wire-table tables shipped under + [`crate::generated`] + +## Wire envelope + +Every frame on the wire is encoded as: + +```text +[requestId: SCALE str][discriminant: u8][payload bytes...] +``` + +The discriminant maps to a method/kind tag via the auto-generated +[`crate::generated::wire_table::WIRE_TABLE`]. Method ordering is part of +the wire protocol; only ever append. + +The payload bytes are the SCALE-encoded inner value, inlined without a +length prefix. In-memory we keep the tag as a `String` so the dispatcher +(which keys on method name) is independent of the wire numbering. diff --git a/host-libs/js/shared/dist/wasm/web/package.json b/host-libs/js/shared/dist/wasm/web/package.json new file mode 100644 index 00000000..cd9729a0 --- /dev/null +++ b/host-libs/js/shared/dist/wasm/web/package.json @@ -0,0 +1,17 @@ +{ + "name": "truapi-server", + "type": "module", + "description": "TrUAPI server runtime: dispatcher, frames, SCALE, streams", + "version": "0.1.0", + "license": "MIT", + "files": [ + "truapi_server_bg.wasm", + "truapi_server.js", + "truapi_server.d.ts" + ], + "main": "truapi_server.js", + "types": "truapi_server.d.ts", + "sideEffects": [ + "./snippets/*" + ] +} \ No newline at end of file diff --git a/host-libs/js/shared/dist/wasm/web/truapi_server.d.ts b/host-libs/js/shared/dist/wasm/web/truapi_server.d.ts new file mode 100644 index 00000000..2731aa6a --- /dev/null +++ b/host-libs/js/shared/dist/wasm/web/truapi_server.d.ts @@ -0,0 +1,149 @@ +/* tslint:disable */ +/* eslint-disable */ + +/** + * JS-callable handle to the TrUAPI core. Constructed once per shell boot. + */ +export class WasmTrUApiCore { + free(): void; + [Symbol.dispose](): void; + /** + * Drop the currently-paired session. + */ + clearActiveSession(): void; + /** + * Tear down the bridge. Invokes the JS-side `dispose` callback so the + * host can drop its end of the wiring. + */ + dispose(): void; + /** + * Build the core from a JS callbacks object. The object must define + * every host capability the [`truapi_platform::Platform`] trait set + * requires (camelCase property names; see the source for the full + * list). + */ + constructor(callbacks: any); + /** + * Push a SCALE-encoded protocol frame into the dispatcher. Responses + * (and subscription items) flow back through the `emitFrame` + * callback. + */ + receiveFromProduct(frame: Uint8Array): Promise; + /** + * Push the currently-paired session into the core. Called by the + * host shell whenever the user pairs / unpairs. `pubkey` must be + * exactly 32 bytes (sr25519 root public key); usernames may be + * null / undefined when the identity record carries no value. + */ + setActiveSession(pubkey: Uint8Array, lite_username?: string | null, full_username?: string | null): void; +} + +/** + * Toggle [`crate::debug_log`] output. Hosts read their `truapi:debug` + * flag (web: localStorage) and call this once during boot. + */ +export function setDebugEnabled(enabled: boolean): void; + +export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; + +export interface InitOutput { + readonly memory: WebAssembly.Memory; + readonly ffi_truapi_server_rust_future_cancel_f32: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_cancel_f64: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_cancel_i16: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_cancel_i32: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_cancel_i64: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_cancel_i8: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_cancel_pointer: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_cancel_rust_buffer: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_cancel_u16: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_cancel_u32: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_cancel_u64: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_cancel_u8: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_cancel_void: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_complete_f32: (a: bigint, b: number) => number; + readonly ffi_truapi_server_rust_future_complete_f64: (a: bigint, b: number) => number; + readonly ffi_truapi_server_rust_future_complete_i16: (a: bigint, b: number) => number; + readonly ffi_truapi_server_rust_future_complete_i32: (a: bigint, b: number) => number; + readonly ffi_truapi_server_rust_future_complete_i64: (a: bigint, b: number) => bigint; + readonly ffi_truapi_server_rust_future_complete_i8: (a: bigint, b: number) => number; + readonly ffi_truapi_server_rust_future_complete_pointer: (a: bigint, b: number) => number; + readonly ffi_truapi_server_rust_future_complete_rust_buffer: (a: number, b: bigint, c: number) => void; + readonly ffi_truapi_server_rust_future_complete_u16: (a: bigint, b: number) => number; + readonly ffi_truapi_server_rust_future_complete_u32: (a: bigint, b: number) => number; + readonly ffi_truapi_server_rust_future_complete_u64: (a: bigint, b: number) => bigint; + readonly ffi_truapi_server_rust_future_complete_u8: (a: bigint, b: number) => number; + readonly ffi_truapi_server_rust_future_complete_void: (a: bigint, b: number) => void; + readonly ffi_truapi_server_rust_future_free_f32: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_free_f64: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_free_i16: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_free_i32: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_free_i64: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_free_i8: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_free_pointer: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_free_rust_buffer: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_free_u16: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_free_u32: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_free_u64: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_free_u8: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_free_void: (a: bigint) => void; + readonly ffi_truapi_server_rust_future_poll_f32: (a: bigint, b: number, c: bigint) => void; + readonly ffi_truapi_server_rust_future_poll_f64: (a: bigint, b: number, c: bigint) => void; + readonly ffi_truapi_server_rust_future_poll_i16: (a: bigint, b: number, c: bigint) => void; + readonly ffi_truapi_server_rust_future_poll_i32: (a: bigint, b: number, c: bigint) => void; + readonly ffi_truapi_server_rust_future_poll_i64: (a: bigint, b: number, c: bigint) => void; + readonly ffi_truapi_server_rust_future_poll_i8: (a: bigint, b: number, c: bigint) => void; + readonly ffi_truapi_server_rust_future_poll_pointer: (a: bigint, b: number, c: bigint) => void; + readonly ffi_truapi_server_rust_future_poll_rust_buffer: (a: bigint, b: number, c: bigint) => void; + readonly ffi_truapi_server_rust_future_poll_u16: (a: bigint, b: number, c: bigint) => void; + readonly ffi_truapi_server_rust_future_poll_u32: (a: bigint, b: number, c: bigint) => void; + readonly ffi_truapi_server_rust_future_poll_u64: (a: bigint, b: number, c: bigint) => void; + readonly ffi_truapi_server_rust_future_poll_u8: (a: bigint, b: number, c: bigint) => void; + readonly ffi_truapi_server_rust_future_poll_void: (a: bigint, b: number, c: bigint) => void; + readonly ffi_truapi_server_rustbuffer_alloc: (a: number, b: bigint, c: number) => void; + readonly ffi_truapi_server_rustbuffer_free: (a: number, b: number) => void; + readonly ffi_truapi_server_rustbuffer_from_bytes: (a: number, b: number, c: number) => void; + readonly ffi_truapi_server_rustbuffer_reserve: (a: number, b: number, c: bigint, d: number) => void; + readonly ffi_truapi_server_uniffi_contract_version: () => number; + readonly __wbg_wasmtruapicore_free: (a: number, b: number) => void; + readonly setDebugEnabled: (a: number) => void; + readonly wasmtruapicore_clearActiveSession: (a: number) => void; + readonly wasmtruapicore_dispose: (a: number) => [number, number]; + readonly wasmtruapicore_new: (a: any) => [number, number, number]; + readonly wasmtruapicore_receiveFromProduct: (a: number, b: number, c: number) => any; + readonly wasmtruapicore_setActiveSession: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number]; + readonly wasm_bindgen__convert__closures_____invoke__h9f03fbb9b0a11e7f: (a: number, b: number, c: any) => [number, number]; + readonly wasm_bindgen__convert__closures_____invoke__h9632106b2100de3d: (a: number, b: number, c: any, d: any) => void; + readonly wasm_bindgen__convert__closures_____invoke__ha3786d1373b8eeae: (a: number, b: number, c: any) => void; + readonly wasm_bindgen__convert__closures_____invoke__ha3786d1373b8eeae_2: (a: number, b: number, c: any) => void; + readonly __wbindgen_malloc: (a: number, b: number) => number; + readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; + readonly __wbindgen_exn_store: (a: number) => void; + readonly __externref_table_alloc: () => number; + readonly __wbindgen_externrefs: WebAssembly.Table; + readonly __wbindgen_destroy_closure: (a: number, b: number) => void; + readonly __externref_table_dealloc: (a: number) => void; + readonly __wbindgen_start: () => void; +} + +export type SyncInitInput = BufferSource | WebAssembly.Module; + +/** + * Instantiates the given `module`, which can either be bytes or + * a precompiled `WebAssembly.Module`. + * + * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated. + * + * @returns {InitOutput} + */ +export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput; + +/** + * If `module_or_path` is {RequestInfo} or {URL}, makes a request and + * for everything else, calls `WebAssembly.instantiate` directly. + * + * @param {{ module_or_path: InitInput | Promise }} module_or_path - Passing `InitInput` directly is deprecated. + * + * @returns {Promise} + */ +export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise } | InitInput | Promise): Promise; diff --git a/host-libs/js/shared/dist/wasm/web/truapi_server.js b/host-libs/js/shared/dist/wasm/web/truapi_server.js new file mode 100644 index 00000000..99f1c012 --- /dev/null +++ b/host-libs/js/shared/dist/wasm/web/truapi_server.js @@ -0,0 +1,627 @@ +/* @ts-self-types="./truapi_server.d.ts" */ + +/** + * JS-callable handle to the TrUAPI core. Constructed once per shell boot. + */ +export class WasmTrUApiCore { + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + WasmTrUApiCoreFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_wasmtruapicore_free(ptr, 0); + } + /** + * Drop the currently-paired session. + */ + clearActiveSession() { + wasm.wasmtruapicore_clearActiveSession(this.__wbg_ptr); + } + /** + * Tear down the bridge. Invokes the JS-side `dispose` callback so the + * host can drop its end of the wiring. + */ + dispose() { + const ret = wasm.wasmtruapicore_dispose(this.__wbg_ptr); + if (ret[1]) { + throw takeFromExternrefTable0(ret[0]); + } + } + /** + * Build the core from a JS callbacks object. The object must define + * every host capability the [`truapi_platform::Platform`] trait set + * requires (camelCase property names; see the source for the full + * list). + * @param {any} callbacks + */ + constructor(callbacks) { + const ret = wasm.wasmtruapicore_new(callbacks); + if (ret[2]) { + throw takeFromExternrefTable0(ret[1]); + } + this.__wbg_ptr = ret[0]; + WasmTrUApiCoreFinalization.register(this, this.__wbg_ptr, this); + return this; + } + /** + * Push a SCALE-encoded protocol frame into the dispatcher. Responses + * (and subscription items) flow back through the `emitFrame` + * callback. + * @param {Uint8Array} frame + * @returns {Promise} + */ + receiveFromProduct(frame) { + const ptr0 = passArray8ToWasm0(frame, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.wasmtruapicore_receiveFromProduct(this.__wbg_ptr, ptr0, len0); + return ret; + } + /** + * Push the currently-paired session into the core. Called by the + * host shell whenever the user pairs / unpairs. `pubkey` must be + * exactly 32 bytes (sr25519 root public key); usernames may be + * null / undefined when the identity record carries no value. + * @param {Uint8Array} pubkey + * @param {string | null} [lite_username] + * @param {string | null} [full_username] + */ + setActiveSession(pubkey, lite_username, full_username) { + const ptr0 = passArray8ToWasm0(pubkey, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + var ptr1 = isLikeNone(lite_username) ? 0 : passStringToWasm0(lite_username, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len1 = WASM_VECTOR_LEN; + var ptr2 = isLikeNone(full_username) ? 0 : passStringToWasm0(full_username, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len2 = WASM_VECTOR_LEN; + const ret = wasm.wasmtruapicore_setActiveSession(this.__wbg_ptr, ptr0, len0, ptr1, len1, ptr2, len2); + if (ret[1]) { + throw takeFromExternrefTable0(ret[0]); + } + } +} +if (Symbol.dispose) WasmTrUApiCore.prototype[Symbol.dispose] = WasmTrUApiCore.prototype.free; + +/** + * Toggle [`crate::debug_log`] output. Hosts read their `truapi:debug` + * flag (web: localStorage) and call this once during boot. + * @param {boolean} enabled + */ +export function setDebugEnabled(enabled) { + wasm.setDebugEnabled(enabled); +} +function __wbg_get_imports() { + const import0 = { + __proto__: null, + __wbg___wbindgen_boolean_get_2304fb8c853028c8: function(arg0) { + const v = arg0; + const ret = typeof(v) === 'boolean' ? v : undefined; + return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0; + }, + __wbg___wbindgen_debug_string_edece8177ad01481: function(arg0, arg1) { + const ret = debugString(arg1); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg___wbindgen_is_function_5cd60d5cf78b4eef: function(arg0) { + const ret = typeof(arg0) === 'function'; + return ret; + }, + __wbg___wbindgen_is_null_2042690d351e14f0: function(arg0) { + const ret = arg0 === null; + return ret; + }, + __wbg___wbindgen_is_undefined_35bb9f4c7fd651d5: function(arg0) { + const ret = arg0 === undefined; + return ret; + }, + __wbg___wbindgen_string_get_d109740c0d18f4d7: function(arg0, arg1) { + const obj = arg1; + const ret = typeof(obj) === 'string' ? obj : undefined; + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg___wbindgen_throw_9c31b086c2b26051: function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }, + __wbg__wbg_cb_unref_3fa391f3fcdb55f8: function(arg0) { + arg0._wbg_cb_unref(); + }, + __wbg_call_13665d9f14390edc: function() { return handleError(function (arg0, arg1) { + const ret = arg0.call(arg1); + return ret; + }, arguments); }, + __wbg_call_dfde26266607c996: function() { return handleError(function (arg0, arg1, arg2) { + const ret = arg0.call(arg1, arg2); + return ret; + }, arguments); }, + __wbg_call_faa0a261f288f846: function() { return handleError(function (arg0, arg1, arg2, arg3) { + const ret = arg0.call(arg1, arg2, arg3); + return ret; + }, arguments); }, + __wbg_error_f085d7e62279b703: function(arg0) { + console.error(arg0); + }, + __wbg_get_dcf82ab8aad1a593: function() { return handleError(function (arg0, arg1) { + const ret = Reflect.get(arg0, arg1); + return ret; + }, arguments); }, + __wbg_instanceof_Error_b3f7e146d654031a: function(arg0) { + let result; + try { + result = arg0 instanceof Error; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }, + __wbg_instanceof_Promise_09012cfa9708520a: function(arg0) { + let result; + try { + result = arg0 instanceof Promise; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }, + __wbg_instanceof_Uint8Array_abd07d4bd221d50b: function(arg0) { + let result; + try { + result = arg0 instanceof Uint8Array; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }, + __wbg_length_56fcd3e2b7e0299d: function(arg0) { + const ret = arg0.length; + return ret; + }, + __wbg_message_324ac511aeaf710e: function(arg0) { + const ret = arg0.message; + return ret; + }, + __wbg_new_from_slice_269e35316ed2d061: function(arg0, arg1) { + const ret = new Uint8Array(getArrayU8FromWasm0(arg0, arg1)); + return ret; + }, + __wbg_new_no_args_f476b292f3fd1dc0: function(arg0, arg1) { + const ret = new Function(getStringFromWasm0(arg0, arg1)); + return ret; + }, + __wbg_new_typed_c072c4ce9a2a0cdf: function(arg0, arg1) { + try { + var state0 = {a: arg0, b: arg1}; + var cb0 = (arg0, arg1) => { + const a = state0.a; + state0.a = 0; + try { + return wasm_bindgen__convert__closures_____invoke__h9632106b2100de3d(a, state0.b, arg0, arg1); + } finally { + state0.a = a; + } + }; + const ret = new Promise(cb0); + return ret; + } finally { + state0.a = 0; + } + }, + __wbg_prototypesetcall_5f9bdc8d75e07276: function(arg0, arg1, arg2) { + Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2); + }, + __wbg_queueMicrotask_78d584b53af520f5: function(arg0) { + const ret = arg0.queueMicrotask; + return ret; + }, + __wbg_queueMicrotask_b39ea83c7f01971a: function(arg0) { + queueMicrotask(arg0); + }, + __wbg_resolve_d17db9352f5a220e: function(arg0) { + const ret = Promise.resolve(arg0); + return ret; + }, + __wbg_static_accessor_GLOBAL_THIS_02344c9b09eb08a9: function() { + const ret = typeof globalThis === 'undefined' ? null : globalThis; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_static_accessor_GLOBAL_ac6d4ac874d5cd54: function() { + const ret = typeof global === 'undefined' ? null : global; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_static_accessor_SELF_9b2406c23aeb2023: function() { + const ret = typeof self === 'undefined' ? null : self; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_static_accessor_WINDOW_b34d2126934e16ba: function() { + const ret = typeof window === 'undefined' ? null : window; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_then_837494e384b37459: function(arg0, arg1) { + const ret = arg0.then(arg1); + return ret; + }, + __wbg_then_bd927500e8905df2: function(arg0, arg1, arg2) { + const ret = arg0.then(arg1, arg2); + return ret; + }, + __wbindgen_cast_0000000000000001: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 312, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__ha3786d1373b8eeae); + return ret; + }, + __wbindgen_cast_0000000000000002: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 388, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h9f03fbb9b0a11e7f); + return ret; + }, + __wbindgen_cast_0000000000000003: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Uint8Array")], shim_idx: 312, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__ha3786d1373b8eeae_2); + return ret; + }, + __wbindgen_cast_0000000000000004: function(arg0, arg1) { + // Cast intrinsic for `Ref(String) -> Externref`. + const ret = getStringFromWasm0(arg0, arg1); + return ret; + }, + __wbindgen_init_externref_table: function() { + const table = wasm.__wbindgen_externrefs; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + }, + }; + return { + __proto__: null, + "./truapi_server_bg.js": import0, + }; +} + +function wasm_bindgen__convert__closures_____invoke__ha3786d1373b8eeae(arg0, arg1, arg2) { + wasm.wasm_bindgen__convert__closures_____invoke__ha3786d1373b8eeae(arg0, arg1, arg2); +} + +function wasm_bindgen__convert__closures_____invoke__ha3786d1373b8eeae_2(arg0, arg1, arg2) { + wasm.wasm_bindgen__convert__closures_____invoke__ha3786d1373b8eeae_2(arg0, arg1, arg2); +} + +function wasm_bindgen__convert__closures_____invoke__h9f03fbb9b0a11e7f(arg0, arg1, arg2) { + const ret = wasm.wasm_bindgen__convert__closures_____invoke__h9f03fbb9b0a11e7f(arg0, arg1, arg2); + if (ret[1]) { + throw takeFromExternrefTable0(ret[0]); + } +} + +function wasm_bindgen__convert__closures_____invoke__h9632106b2100de3d(arg0, arg1, arg2, arg3) { + wasm.wasm_bindgen__convert__closures_____invoke__h9632106b2100de3d(arg0, arg1, arg2, arg3); +} + +const WasmTrUApiCoreFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_wasmtruapicore_free(ptr, 1)); + +function addToExternrefTable0(obj) { + const idx = wasm.__externref_table_alloc(); + wasm.__wbindgen_externrefs.set(idx, obj); + return idx; +} + +const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(state => wasm.__wbindgen_destroy_closure(state.a, state.b)); + +function debugString(val) { + // primitive types + const type = typeof val; + if (type == 'number' || type == 'boolean' || val == null) { + return `${val}`; + } + if (type == 'string') { + return `"${val}"`; + } + if (type == 'symbol') { + const description = val.description; + if (description == null) { + return 'Symbol'; + } else { + return `Symbol(${description})`; + } + } + if (type == 'function') { + const name = val.name; + if (typeof name == 'string' && name.length > 0) { + return `Function(${name})`; + } else { + return 'Function'; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = '['; + if (length > 0) { + debug += debugString(val[0]); + } + for(let i = 1; i < length; i++) { + debug += ', ' + debugString(val[i]); + } + debug += ']'; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches && builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == 'Object') { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return 'Object(' + JSON.stringify(val) + ')'; + } catch (_) { + return 'Object'; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} + +function getArrayU8FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); +} + +let cachedDataViewMemory0 = null; +function getDataViewMemory0() { + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} + +function getStringFromWasm0(ptr, len) { + return decodeText(ptr >>> 0, len); +} + +let cachedUint8ArrayMemory0 = null; +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + const idx = addToExternrefTable0(e); + wasm.__wbindgen_exn_store(idx); + } +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +function makeMutClosure(arg0, arg1, f) { + const state = { a: arg0, b: arg1, cnt: 1 }; + const real = (...args) => { + + // First up with a closure we increment the internal reference + // count. This ensures that the Rust closure environment won't + // be deallocated while we're invoking it. + state.cnt++; + const a = state.a; + state.a = 0; + try { + return f(a, state.b, ...args); + } finally { + state.a = a; + real._wbg_cb_unref(); + } + }; + real._wbg_cb_unref = () => { + if (--state.cnt === 0) { + wasm.__wbindgen_destroy_closure(state.a, state.b); + state.a = 0; + CLOSURE_DTORS.unregister(state); + } + }; + CLOSURE_DTORS.register(real, state, state); + return real; +} + +function passArray8ToWasm0(arg, malloc) { + const ptr = malloc(arg.length * 1, 1) >>> 0; + getUint8ArrayMemory0().set(arg, ptr / 1); + WASM_VECTOR_LEN = arg.length; + return ptr; +} + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = cachedTextEncoder.encodeInto(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function takeFromExternrefTable0(idx) { + const value = wasm.__wbindgen_externrefs.get(idx); + wasm.__externref_table_dealloc(idx); + return value; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +cachedTextDecoder.decode(); +const MAX_SAFARI_DECODE_BYTES = 2146435072; +let numBytesDecoded = 0; +function decodeText(ptr, len) { + numBytesDecoded += len; + if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { + cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + cachedTextDecoder.decode(); + numBytesDecoded = len; + } + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +const cachedTextEncoder = new TextEncoder(); + +if (!('encodeInto' in cachedTextEncoder)) { + cachedTextEncoder.encodeInto = function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; + }; +} + +let WASM_VECTOR_LEN = 0; + +let wasmModule, wasmInstance, wasm; +function __wbg_finalize_init(instance, module) { + wasmInstance = instance; + wasm = instance.exports; + wasmModule = module; + cachedDataViewMemory0 = null; + cachedUint8ArrayMemory0 = null; + wasm.__wbindgen_start(); + return wasm; +} + +async function __wbg_load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + } catch (e) { + const validResponse = module.ok && expectedResponseType(module.type); + + if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { throw e; } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + } else { + return instance; + } + } + + function expectedResponseType(type) { + switch (type) { + case 'basic': case 'cors': case 'default': return true; + } + return false; + } +} + +function initSync(module) { + if (wasm !== undefined) return wasm; + + + if (module !== undefined) { + if (Object.getPrototypeOf(module) === Object.prototype) { + ({module} = module) + } else { + console.warn('using deprecated parameters for `initSync()`; pass a single object instead') + } + } + + const imports = __wbg_get_imports(); + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + const instance = new WebAssembly.Instance(module, imports); + return __wbg_finalize_init(instance, module); +} + +async function __wbg_init(module_or_path) { + if (wasm !== undefined) return wasm; + + + if (module_or_path !== undefined) { + if (Object.getPrototypeOf(module_or_path) === Object.prototype) { + ({module_or_path} = module_or_path) + } else { + console.warn('using deprecated parameters for the initialization function; pass a single object instead') + } + } + + if (module_or_path === undefined) { + module_or_path = new URL('truapi_server_bg.wasm', import.meta.url); + } + const imports = __wbg_get_imports(); + + if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { + module_or_path = fetch(module_or_path); + } + + const { instance, module } = await __wbg_load(await module_or_path, imports); + + return __wbg_finalize_init(instance, module); +} + +export { initSync, __wbg_init as default }; diff --git a/host-libs/js/shared/dist/wasm/web/truapi_server_bg.wasm b/host-libs/js/shared/dist/wasm/web/truapi_server_bg.wasm new file mode 100644 index 0000000000000000000000000000000000000000..7fb39588d1b87d83aa418bb32d2e30fffc79d3b3 GIT binary patch literal 943201 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ÐCtwmo3f%RCs)2e1dJja7(EN=p~_6 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

$$HXky+qL~?Jhm(ST!i^G>2WmCx=;yaa*&Ve} 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≫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 z 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 Gng0SQorbai literal 0 HcmV?d00001 diff --git a/host-libs/js/shared/dist/wasm/web/truapi_server_bg.wasm.d.ts b/host-libs/js/shared/dist/wasm/web/truapi_server_bg.wasm.d.ts new file mode 100644 index 00000000..478d7602 --- /dev/null +++ b/host-libs/js/shared/dist/wasm/web/truapi_server_bg.wasm.d.ts @@ -0,0 +1,79 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export const ffi_truapi_server_rust_future_cancel_f32: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_f64: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_i16: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_i32: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_i64: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_i8: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_pointer: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_rust_buffer: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_u16: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_u32: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_u64: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_u8: (a: bigint) => void; +export const ffi_truapi_server_rust_future_cancel_void: (a: bigint) => void; +export const ffi_truapi_server_rust_future_complete_f32: (a: bigint, b: number) => number; +export const ffi_truapi_server_rust_future_complete_f64: (a: bigint, b: number) => number; +export const ffi_truapi_server_rust_future_complete_i16: (a: bigint, b: number) => number; +export const ffi_truapi_server_rust_future_complete_i32: (a: bigint, b: number) => number; +export const ffi_truapi_server_rust_future_complete_i64: (a: bigint, b: number) => bigint; +export const ffi_truapi_server_rust_future_complete_i8: (a: bigint, b: number) => number; +export const ffi_truapi_server_rust_future_complete_pointer: (a: bigint, b: number) => number; +export const ffi_truapi_server_rust_future_complete_rust_buffer: (a: number, b: bigint, c: number) => void; +export const ffi_truapi_server_rust_future_complete_u16: (a: bigint, b: number) => number; +export const ffi_truapi_server_rust_future_complete_u32: (a: bigint, b: number) => number; +export const ffi_truapi_server_rust_future_complete_u64: (a: bigint, b: number) => bigint; +export const ffi_truapi_server_rust_future_complete_u8: (a: bigint, b: number) => number; +export const ffi_truapi_server_rust_future_complete_void: (a: bigint, b: number) => void; +export const ffi_truapi_server_rust_future_free_f32: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_f64: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_i16: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_i32: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_i64: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_i8: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_pointer: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_rust_buffer: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_u16: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_u32: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_u64: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_u8: (a: bigint) => void; +export const ffi_truapi_server_rust_future_free_void: (a: bigint) => void; +export const ffi_truapi_server_rust_future_poll_f32: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_f64: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_i16: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_i32: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_i64: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_i8: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_pointer: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_rust_buffer: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_u16: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_u32: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_u64: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_u8: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rust_future_poll_void: (a: bigint, b: number, c: bigint) => void; +export const ffi_truapi_server_rustbuffer_alloc: (a: number, b: bigint, c: number) => void; +export const ffi_truapi_server_rustbuffer_free: (a: number, b: number) => void; +export const ffi_truapi_server_rustbuffer_from_bytes: (a: number, b: number, c: number) => void; +export const ffi_truapi_server_rustbuffer_reserve: (a: number, b: number, c: bigint, d: number) => void; +export const ffi_truapi_server_uniffi_contract_version: () => number; +export const __wbg_wasmtruapicore_free: (a: number, b: number) => void; +export const setDebugEnabled: (a: number) => void; +export const wasmtruapicore_clearActiveSession: (a: number) => void; +export const wasmtruapicore_dispose: (a: number) => [number, number]; +export const wasmtruapicore_new: (a: any) => [number, number, number]; +export const wasmtruapicore_receiveFromProduct: (a: number, b: number, c: number) => any; +export const wasmtruapicore_setActiveSession: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number]; +export const wasm_bindgen__convert__closures_____invoke__h9f03fbb9b0a11e7f: (a: number, b: number, c: any) => [number, number]; +export const wasm_bindgen__convert__closures_____invoke__h9632106b2100de3d: (a: number, b: number, c: any, d: any) => void; +export const wasm_bindgen__convert__closures_____invoke__ha3786d1373b8eeae: (a: number, b: number, c: any) => void; +export const wasm_bindgen__convert__closures_____invoke__ha3786d1373b8eeae_2: (a: number, b: number, c: any) => void; +export const __wbindgen_malloc: (a: number, b: number) => number; +export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; +export const __wbindgen_exn_store: (a: number) => void; +export const __externref_table_alloc: () => number; +export const __wbindgen_externrefs: WebAssembly.Table; +export const __wbindgen_destroy_closure: (a: number, b: number) => void; +export const __externref_table_dealloc: (a: number) => void; +export const __wbindgen_start: () => void; diff --git a/host-libs/js/shared/package-lock.json b/host-libs/js/shared/package-lock.json new file mode 100644 index 00000000..6114ed15 --- /dev/null +++ b/host-libs/js/shared/package-lock.json @@ -0,0 +1,67 @@ +{ + "name": "@parity/host-shared", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@parity/host-shared", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@parity/truapi": "file:../../../js/packages/truapi", + "@parity/truapi-host": "file:../../../js/packages/truapi-host" + }, + "devDependencies": { + "typescript": "^5.7" + } + }, + "../../../js/packages/truapi": { + "name": "@parity/truapi", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "neverthrow": "^8.2.0", + "scale-ts": "^1.6.1" + }, + "devDependencies": { + "typescript": "^5.7" + } + }, + "../../../js/packages/truapi-host": { + "name": "@parity/truapi-host", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@parity/truapi": "file:../truapi", + "neverthrow": "^8.2.0", + "scale-ts": "^1.6.1" + }, + "devDependencies": { + "typescript": "^5.7" + } + }, + "node_modules/@parity/truapi": { + "resolved": "../../../js/packages/truapi", + "link": true + }, + "node_modules/@parity/truapi-host": { + "resolved": "../../../js/packages/truapi-host", + "link": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/host-libs/js/shared/package.json b/host-libs/js/shared/package.json new file mode 100644 index 00000000..2f025585 --- /dev/null +++ b/host-libs/js/shared/package.json @@ -0,0 +1,47 @@ +{ + "name": "@parity/host-shared", + "version": "0.1.0", + "description": "Shared TrUAPI host runtime: WASM-backed provider, dispatcher adapters, worker entry-point", + "license": "MIT", + "author": "Parity Technologies ", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./worker-runtime": { + "types": "./dist/worker-runtime.d.ts", + "import": "./dist/worker-runtime.js" + }, + "./node-runtime": { + "types": "./dist/node-runtime.d.ts", + "import": "./dist/node-runtime.js" + }, + "./wasm/web": { + "import": "./dist/wasm/web/truapi_server.js" + }, + "./wasm/node": { + "import": "./dist/wasm/node/truapi_server.js" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "tsc", + "build:wasm": "node scripts/build-wasm.mjs", + "prepare": "npm run build", + "test": "for f in test/*.test.mjs; do echo \"=== $f ===\" && node --test \"$f\" || exit 1; done" + }, + "dependencies": { + "@parity/truapi": "file:../../../js/packages/truapi", + "@parity/truapi-host": "file:../../../js/packages/truapi-host" + }, + "devDependencies": { + "typescript": "^5.7" + } +} diff --git a/host-libs/js/shared/scripts/build-wasm.mjs b/host-libs/js/shared/scripts/build-wasm.mjs new file mode 100644 index 00000000..5a48439c --- /dev/null +++ b/host-libs/js/shared/scripts/build-wasm.mjs @@ -0,0 +1,42 @@ +#!/usr/bin/env node +// Rebuild the truapi-server WASM artefacts committed under +// `dist/wasm/{web,node}/`. wasm-pack is required. + +import { execFile } from "node:child_process"; +import { rm } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); +const __dirname = dirname(fileURLToPath(import.meta.url)); +const pkgRoot = resolve(__dirname, ".."); +const repoRoot = resolve(pkgRoot, "../../.."); +const rustCrate = resolve(repoRoot, "rust/crates/truapi-server"); + +function args(target, outDir) { + return [ + "build", + rustCrate, + "--target", + target, + "--out-dir", + outDir, + "--out-name", + "truapi_server", + "--no-default-features", + ]; +} + +async function build(target, subdir) { + const outDir = resolve(pkgRoot, "dist/wasm", subdir); + process.stdout.write(`wasm-pack build --target ${target} → ${outDir}\n`); + await execFileAsync("wasm-pack", args(target, outDir), { cwd: repoRoot }); + // wasm-pack writes a `.gitignore: *` next to the artefacts which would + // hide the committed bundle. Remove it; the package's outer .gitignore + // handles compiled TS without masking the WASM files. + await rm(resolve(outDir, ".gitignore"), { force: true }); +} + +await build("web", "web"); +await build("nodejs", "node"); diff --git a/host-libs/js/shared/src/dispatcher.ts b/host-libs/js/shared/src/dispatcher.ts new file mode 100644 index 00000000..52427d30 --- /dev/null +++ b/host-libs/js/shared/src/dispatcher.ts @@ -0,0 +1,20 @@ +// Generic dispatcher utilities sit in `@parity/truapi-host`. This module +// re-exports them so hosts that depend on `@parity/host-shared` get the +// dispatcher entry-point without a separate install. + +export { + createHostServer, + toFlatResponsePayload, + toResponsePayload, +} from "@parity/truapi-host"; + +export type { + CallContext, + HostDispatchEntry, + HostServerHooks, + RequestEntry, + SubscriptionCleanup, + SubscriptionEntry, + SubscriptionFramePort, + TrUApiHostServer, +} from "@parity/truapi-host"; diff --git a/host-libs/js/shared/src/index.ts b/host-libs/js/shared/src/index.ts new file mode 100644 index 00000000..e86740ba --- /dev/null +++ b/host-libs/js/shared/src/index.ts @@ -0,0 +1,41 @@ +export type { + Payload, + ProtocolMessage, + Provider, + HostPermissionKind, +} from "./types.js"; + +export type { + Awaitable, + ChainConnect, + ChainConnection, + WasmCoreLike, + WasmRawCallbacks, +} from "./runtime.js"; +export { createUnavailableCallbacks, createWasmProvider } from "./runtime.js"; + +export { createNodeWasmProvider } from "./node-runtime.js"; + +export type { + CallbackArgs, + CallbackName, + MainToWorker, + SubscriptionName, + WorkerToMain, +} from "./worker-protocol.js"; + +export type { + CallContext, + HostDispatchEntry, + HostServerHooks, + RequestEntry, + SubscriptionCleanup, + SubscriptionEntry, + SubscriptionFramePort, + TrUApiHostServer, +} from "./dispatcher.js"; +export { + createHostServer, + toFlatResponsePayload, + toResponsePayload, +} from "./dispatcher.js"; diff --git a/host-libs/js/shared/src/node-runtime.ts b/host-libs/js/shared/src/node-runtime.ts new file mode 100644 index 00000000..b18c5378 --- /dev/null +++ b/host-libs/js/shared/src/node-runtime.ts @@ -0,0 +1,44 @@ +import type { Provider } from "@parity/truapi"; + +import { + createWasmProvider, + type WasmCoreLike, + type WasmRawCallbacks, +} from "./runtime.js"; + +interface NodeWasmModuleShape { + WasmTrUApiCore: new (callbacks: unknown) => WasmCoreLike; +} + +/** + * Lazy-load the node-targeted WASM bundle and wrap it in a `Provider`. + * + * The bundle initialises synchronously (wasm-pack nodejs target uses + * `require()` under the hood for the .wasm file), so callers receive + * a ready-to-use provider once the dynamic import resolves. + */ +export async function createNodeWasmProvider( + partial: Omit, +): Promise { + // Dynamic import keeps the WASM module out of the package's static + // dependency graph and out of the tsc rootDir. Indirected through a + // variable so TS skips the static module-existence check. + const wasmNodePath = "./wasm/node/truapi_server.js"; + const mod = (await import( + /* @vite-ignore */ wasmNodePath + )) as NodeWasmModuleShape | { default: NodeWasmModuleShape }; + + const wasm: NodeWasmModuleShape = + "WasmTrUApiCore" in mod + ? (mod as NodeWasmModuleShape) + : (mod.default as NodeWasmModuleShape); + + if (!wasm?.WasmTrUApiCore) { + throw new Error("Node WASM bundle did not export WasmTrUApiCore"); + } + + return createWasmProvider( + (raw) => new wasm.WasmTrUApiCore(raw), + partial, + ); +} diff --git a/host-libs/js/shared/src/runtime.ts b/host-libs/js/shared/src/runtime.ts new file mode 100644 index 00000000..e766e869 --- /dev/null +++ b/host-libs/js/shared/src/runtime.ts @@ -0,0 +1,191 @@ +import type { Provider } from "@parity/truapi"; + +/** + * Async-or-sync return. Synchronous hosts (e.g. the dotli main-thread + * shell hitting localStorage) can return a plain value; the WASM bridge + * awaits every return so an `async` impl also works. + */ +export type Awaitable = T | Promise; + +/** + * Open a JSON-RPC connection for `genesisHash`. The wasm bridge passes + * `onResponse` so the host can push smoldot replies back asynchronously. + * Returning `null` (or throwing) tells the core no provider is available. + */ +export type ChainConnect = ( + genesisHash: string, + onResponse: (json: string) => void, +) => Awaitable; + +/** + * Per-connection handle returned by `chainConnect`. `send` forwards a + * SCALE-encoded JSON-RPC request; `close` tears the connection down. + */ +export interface ChainConnection { + send(request: string): void; + close(): void; +} + +/** + * Raw byte-oriented callbacks the WASM core invokes. Names match the + * camelCase property keys the Rust `JsBridge::from_js` extracts. Request + * callbacks return `Promise` (or `Promise` for the + * permission prompts); subscription callbacks accept a `sendItem` sink + * and return an optional `dispose` function. + */ +export interface WasmRawCallbacks { + navigateTo(url: string): Promise; + pushNotification(payload: Uint8Array): Promise; + devicePermission(payload: Uint8Array): Promise; + remotePermission(payload: Uint8Array): Promise; + featureSupported(payload: Uint8Array): Promise; + localStorageRead(key: string): Promise; + localStorageWrite(key: string, value: Uint8Array): Promise; + localStorageClear(key: string): Promise; + accountGet(payload: Uint8Array): Promise; + accountGetAlias(payload: Uint8Array): Promise; + accountCreateProof(payload: Uint8Array): Promise; + getLegacyAccounts(payload: Uint8Array): Promise; + accountConnectionStatusSubscribe( + sendItem: (bytes: Uint8Array) => void, + ): (() => void) | void; + getUserId(payload: Uint8Array): Promise; + signPayload(payload: Uint8Array): Promise; + signRaw(payload: Uint8Array): Promise; + statementStoreSubscribe( + payload: Uint8Array, + sendItem: (bytes: Uint8Array) => void, + ): (() => void) | void; + statementStoreSubmit(payload: Uint8Array): Promise; + statementStoreCreateProof(payload: Uint8Array): Promise; + preimageLookupSubscribe( + payload: Uint8Array, + sendItem: (bytes: Uint8Array) => void, + ): (() => void) | void; + /** Optional. When omitted, the WASM bridge reports chain calls as + * "unavailable". Hosts that own chain access (e.g. dotli's + * smoldot/RPC toggle) supply it. */ + chainConnect?: ChainConnect; + emitFrame(frame: Uint8Array): void; + dispose?(): void; +} + +/** + * Stubs every required callback so a host can spread them over its own + * implementation and override only what it supports. Unavailable methods + * reject with a descriptive error; unavailable subscriptions resolve to + * no-op start handlers. + */ +export function createUnavailableCallbacks(): Omit< + WasmRawCallbacks, + "emitFrame" | "dispose" | "chainConnect" +> { + const unavailable = + (method: string) => + async (): Promise => { + throw new Error(`${method} unavailable on this host`); + }; + const noopSubscribe = (): void => {}; + return { + navigateTo: unavailable("navigateTo"), + pushNotification: unavailable("pushNotification"), + devicePermission: async () => false, + remotePermission: async () => false, + featureSupported: unavailable("featureSupported"), + localStorageRead: async () => undefined, + localStorageWrite: async () => {}, + localStorageClear: async () => {}, + accountGet: unavailable("accountGet"), + accountGetAlias: unavailable("accountGetAlias"), + accountCreateProof: unavailable("accountCreateProof"), + getLegacyAccounts: unavailable("getLegacyAccounts"), + accountConnectionStatusSubscribe: noopSubscribe, + getUserId: unavailable("getUserId"), + signPayload: unavailable("signPayload"), + signRaw: unavailable("signRaw"), + statementStoreSubscribe: noopSubscribe, + statementStoreSubmit: unavailable("statementStoreSubmit"), + statementStoreCreateProof: unavailable("statementStoreCreateProof"), + preimageLookupSubscribe: noopSubscribe, + }; +} + +/** + * Shape exposed by the wasm-pack output's `WasmTrUApiCore`. Kept local + * so the package does not have a hard dependency on the generated `.d.ts` + * file path. + */ +export interface WasmCoreLike { + receiveFromProduct(frame: Uint8Array): Promise; + dispose(): void; + free(): void; +} + +/** + * Wraps a WASM core in a `Provider`, the byte transport abstraction + * exposed by `@parity/truapi`. The provider can be handed to + * `createHostServer` from `@parity/truapi-host` so the dispatcher dispatches + * inbound frames into the WASM core and forwards core-emitted frames back + * to the listener registered through `provider.subscribe`. + */ +export function createWasmProvider( + createCore: (rawCallbacks: WasmRawCallbacks) => WasmCoreLike, + partial: Omit, +): Provider { + const listeners = new Set<(message: Uint8Array) => void>(); + const closeListeners = new Set<(error: Error) => void>(); + let disposed = false; + + const raw: WasmRawCallbacks = { + ...partial, + emitFrame(frame: Uint8Array) { + if (disposed) return; + // Copy out of the WASM-owned buffer so retained references stay + // valid once the core reuses the underlying memory. + const copy = new Uint8Array(frame.length); + copy.set(frame); + for (const listener of [...listeners]) listener(copy); + }, + }; + + const core = createCore(raw); + + return { + postMessage(bytes: Uint8Array): void { + if (disposed) return; + void core.receiveFromProduct(bytes).catch((err: unknown) => { + const error = err instanceof Error ? err : new Error(String(err)); + for (const listener of [...closeListeners]) listener(error); + }); + }, + subscribe(callback) { + listeners.add(callback); + return () => { + listeners.delete(callback); + }; + }, + subscribeClose(callback) { + closeListeners.add(callback); + return () => { + closeListeners.delete(callback); + }; + }, + dispose() { + if (disposed) return; + disposed = true; + try { + core.dispose(); + } catch { + // host dispose threw, swallow during teardown + } + try { + core.free(); + } catch { + // already freed + } + listeners.clear(); + closeListeners.clear(); + partial.dispose?.(); + }, + }; +} diff --git a/host-libs/js/shared/src/types.ts b/host-libs/js/shared/src/types.ts new file mode 100644 index 00000000..6ee1216c --- /dev/null +++ b/host-libs/js/shared/src/types.ts @@ -0,0 +1,9 @@ +import type { Payload, ProtocolMessage, Provider } from "@parity/truapi"; + +export type { Payload, ProtocolMessage, Provider }; + +/** + * Subset of permission tags the host can be asked to prompt for. Mirrors + * the Rust `Permission` enum that flows through the WASM bridge. + */ +export type HostPermissionKind = "Device" | "Remote"; diff --git a/host-libs/js/shared/src/worker-protocol.ts b/host-libs/js/shared/src/worker-protocol.ts new file mode 100644 index 00000000..9c8d30d1 --- /dev/null +++ b/host-libs/js/shared/src/worker-protocol.ts @@ -0,0 +1,76 @@ +// Wire format between the main thread (`createWebWorkerProvider`) and the +// Web Worker that hosts the truapi-server WASM core. +// +// Frames (`kind: 'frame'`) carry SCALE-encoded `ProtocolMessage` bytes +// untouched in either direction. Everything else is a control message +// for callback dispatch, subscription bookkeeping, or chain connections. + +/** + * Names of every request/response style host callback the wasm core can + * invoke. Names match the camelCase property keys of `WasmRawCallbacks`. + */ +export type CallbackName = + | "navigateTo" + | "pushNotification" + | "devicePermission" + | "remotePermission" + | "featureSupported" + | "localStorageRead" + | "localStorageWrite" + | "localStorageClear" + | "accountGet" + | "accountGetAlias" + | "accountCreateProof" + | "getLegacyAccounts" + | "getUserId" + | "signPayload" + | "signRaw" + | "statementStoreSubmit" + | "statementStoreCreateProof"; + +/** + * Names of every subscription host callback. Each has the shape + * `(payload?, sendItem) => dispose | void`. + */ +export type SubscriptionName = + | "accountConnectionStatusSubscribe" + | "statementStoreSubscribe" + | "preimageLookupSubscribe"; + +/** + * Positional arguments for a callback. The wasm core calls each callback + * at a fixed arity; a uniform `unknown[]` keeps the wire protocol simple. + */ +export type CallbackArgs = readonly unknown[]; + +export type MainToWorker = + | { kind: "configure"; debug: boolean } + | { kind: "frame"; bytes: Uint8Array } + | { kind: "callbackResponse"; requestId: number; ok: true; value: unknown } + | { kind: "callbackResponse"; requestId: number; ok: false; error: string } + | { kind: "subscriptionItem"; subId: number; bytes: Uint8Array } + | { kind: "chainConnectAck"; connId: number; ok: true } + | { kind: "chainConnectAck"; connId: number; ok: false; error: string } + | { kind: "chainResponse"; connId: number; json: string } + | { kind: "dispose" }; + +export type WorkerToMain = + | { kind: "ready" } + | { kind: "error"; error: string } + | { kind: "frame"; bytes: Uint8Array } + | { + kind: "callbackRequest"; + requestId: number; + name: CallbackName; + args: CallbackArgs; + } + | { + kind: "subscriptionStart"; + subId: number; + name: SubscriptionName; + payload: Uint8Array | null; + } + | { kind: "subscriptionStop"; subId: number } + | { kind: "chainConnectStart"; connId: number; genesisHash: string } + | { kind: "chainSend"; connId: number; request: string } + | { kind: "chainClose"; connId: number }; diff --git a/host-libs/js/shared/src/worker-runtime.ts b/host-libs/js/shared/src/worker-runtime.ts new file mode 100644 index 00000000..29f85729 --- /dev/null +++ b/host-libs/js/shared/src/worker-runtime.ts @@ -0,0 +1,259 @@ +/// +// Worker entrypoint. Loads the web-targeted truapi-server WASM bundle and +// bridges every host callback over postMessage. The main thread keeps the +// state that needs DOM access (localStorage, prompts) while the CPU-heavy +// smoldot/dispatcher work runs here off the page main thread. + +import type { + CallbackName, + MainToWorker, + SubscriptionName, + WorkerToMain, +} from "./worker-protocol.js"; + +interface WasmCore { + receiveFromProduct(frame: Uint8Array): Promise; + dispose(): void; + free(): void; +} + +interface WasmModuleShape { + default: (input?: unknown) => Promise; + WasmTrUApiCore: new (callbacks: unknown) => WasmCore; + setDebugEnabled: (enabled: boolean) => void; +} + +// Resolved at runtime — the wasm-pack artifact lives outside `src/` so a +// static import would leak into the TS rootDir. The relative path is +// resolved against `dist/worker-runtime.js` once compiled. Indirected +// through a variable so TS skips the static module-existence check. +const WASM_WEB_PATH = "./wasm/web/truapi_server.js"; +const wasmModulePromise = import( + /* @vite-ignore */ WASM_WEB_PATH +) as Promise; + +const ctx = self as unknown as DedicatedWorkerGlobalScope; + +function postToMain(msg: WorkerToMain): void { + ctx.postMessage(msg); +} + +function errMsg(err: unknown): string { + if (err instanceof Error) return err.message; + if (typeof err === "string") return err; + return JSON.stringify(err); +} + +let nextRequestId = 0; +const pendingCallbacks = new Map< + number, + ( + result: { ok: true; value: unknown } | { ok: false; error: string }, + ) => void +>(); + +let nextSubId = 0; +const subscriptionItemListeners = new Map< + number, + (bytes: Uint8Array) => void +>(); + +let nextConnId = 0; +type ChainConnectAck = { ok: true } | { ok: false; error: string }; +const chainConnectAcks = new Map void>(); +const chainResponseListeners = new Map void>(); + +function callbackRequest( + name: CallbackName, + args: readonly unknown[], +): Promise { + return new Promise((resolve, reject) => { + const requestId = ++nextRequestId; + pendingCallbacks.set(requestId, (r) => { + if (r.ok) resolve(r.value); + else reject(new Error(r.error)); + }); + postToMain({ kind: "callbackRequest", requestId, name, args }); + }); +} + +function startSubscription( + name: SubscriptionName, + payload: Uint8Array | null, + sendItem: (bytes: Uint8Array) => void, +): () => void { + const subId = ++nextSubId; + subscriptionItemListeners.set(subId, sendItem); + postToMain({ kind: "subscriptionStart", subId, name, payload }); + return () => { + subscriptionItemListeners.delete(subId); + postToMain({ kind: "subscriptionStop", subId }); + }; +} + +interface WorkerChainConnection { + send(request: string): void; + close(): void; +} + +function chainConnect( + genesisHash: string, + onResponse: (json: string) => void, +): Promise { + const connId = ++nextConnId; + return new Promise((resolve, reject) => { + chainConnectAcks.set(connId, (ack) => { + if (!ack.ok) { + chainResponseListeners.delete(connId); + reject(new Error(ack.error)); + return; + } + resolve({ + send(request: string) { + postToMain({ kind: "chainSend", connId, request }); + }, + close() { + chainResponseListeners.delete(connId); + postToMain({ kind: "chainClose", connId }); + }, + }); + }); + chainResponseListeners.set(connId, onResponse); + postToMain({ kind: "chainConnectStart", connId, genesisHash }); + }); +} + +const rawCallbacks = { + navigateTo: (url: string) => callbackRequest("navigateTo", [url]), + pushNotification: (payload: Uint8Array) => + callbackRequest("pushNotification", [payload]), + devicePermission: (payload: Uint8Array) => + callbackRequest("devicePermission", [payload]) as Promise, + remotePermission: (payload: Uint8Array) => + callbackRequest("remotePermission", [payload]) as Promise, + featureSupported: (payload: Uint8Array) => + callbackRequest("featureSupported", [payload]) as Promise, + localStorageRead: (key: string) => + callbackRequest("localStorageRead", [key]) as Promise< + Uint8Array | null | undefined + >, + localStorageWrite: (key: string, value: Uint8Array) => + callbackRequest("localStorageWrite", [key, value]), + localStorageClear: (key: string) => + callbackRequest("localStorageClear", [key]), + accountGet: (payload: Uint8Array) => + callbackRequest("accountGet", [payload]) as Promise, + accountGetAlias: (payload: Uint8Array) => + callbackRequest("accountGetAlias", [payload]) as Promise, + accountCreateProof: (payload: Uint8Array) => + callbackRequest("accountCreateProof", [payload]) as Promise, + getLegacyAccounts: (payload: Uint8Array) => + callbackRequest("getLegacyAccounts", [payload]) as Promise, + accountConnectionStatusSubscribe: (sendItem: (bytes: Uint8Array) => void) => + startSubscription("accountConnectionStatusSubscribe", null, sendItem), + getUserId: (payload: Uint8Array) => + callbackRequest("getUserId", [payload]) as Promise, + signPayload: (payload: Uint8Array) => + callbackRequest("signPayload", [payload]) as Promise, + signRaw: (payload: Uint8Array) => + callbackRequest("signRaw", [payload]) as Promise, + statementStoreSubscribe: ( + payload: Uint8Array, + sendItem: (bytes: Uint8Array) => void, + ) => startSubscription("statementStoreSubscribe", payload, sendItem), + statementStoreSubmit: (payload: Uint8Array) => + callbackRequest("statementStoreSubmit", [payload]) as Promise, + statementStoreCreateProof: (payload: Uint8Array) => + callbackRequest( + "statementStoreCreateProof", + [payload], + ) as Promise, + preimageLookupSubscribe: ( + payload: Uint8Array, + sendItem: (bytes: Uint8Array) => void, + ) => startSubscription("preimageLookupSubscribe", payload, sendItem), + chainConnect, + emitFrame(frame: Uint8Array): void { + postToMain({ kind: "frame", bytes: frame }); + }, + dispose(): void { + // Main thread terminates the worker, no separate cleanup needed here. + }, +}; + +let core: WasmCore | null = null; +let wasm: WasmModuleShape | null = null; + +(async () => { + try { + wasm = await wasmModulePromise; + await wasm.default(); + core = new wasm.WasmTrUApiCore(rawCallbacks); + postToMain({ kind: "ready" }); + } catch (err) { + postToMain({ kind: "error", error: errMsg(err) }); + } +})(); + +ctx.addEventListener("message", (ev: MessageEvent) => { + const msg = ev.data; + switch (msg.kind) { + case "configure": + wasm?.setDebugEnabled(msg.debug); + break; + case "frame": + void handleFrame(msg.bytes); + break; + case "callbackResponse": { + const cb = pendingCallbacks.get(msg.requestId); + if (cb) { + pendingCallbacks.delete(msg.requestId); + cb( + msg.ok + ? { ok: true, value: msg.value } + : { ok: false, error: msg.error }, + ); + } + break; + } + case "subscriptionItem": { + const listener = subscriptionItemListeners.get(msg.subId); + if (listener) listener(msg.bytes); + break; + } + case "chainConnectAck": { + const cb = chainConnectAcks.get(msg.connId); + if (cb) { + chainConnectAcks.delete(msg.connId); + cb(msg.ok ? { ok: true } : { ok: false, error: msg.error }); + } + break; + } + case "chainResponse": { + const listener = chainResponseListeners.get(msg.connId); + if (listener) listener(msg.json); + break; + } + case "dispose": + try { + core?.dispose(); + core?.free(); + } catch (err) { + postToMain({ kind: "error", error: `dispose: ${errMsg(err)}` }); + } + core = null; + break; + } +}); + +async function handleFrame(bytes: Uint8Array): Promise { + if (!core) { + postToMain({ kind: "error", error: "frame received before core is ready" }); + return; + } + try { + await core.receiveFromProduct(bytes); + } catch (err) { + postToMain({ kind: "error", error: `receiveFromProduct: ${errMsg(err)}` }); + } +} diff --git a/host-libs/js/shared/test/dispatcher-roundtrip.test.mjs b/host-libs/js/shared/test/dispatcher-roundtrip.test.mjs new file mode 100644 index 00000000..ed64fb2f --- /dev/null +++ b/host-libs/js/shared/test/dispatcher-roundtrip.test.mjs @@ -0,0 +1,79 @@ +// Smoke test that the dispatcher re-export from @parity/host-shared +// routes inbound request frames to a registered handler and emits a +// response frame back through the provider, end-to-end. + +import assert from "node:assert/strict"; +import test from "node:test"; + +import { encodeWireMessage, decodeWireMessage } from "@parity/truapi"; +import { createHostServer } from "../dist/index.js"; + +function makeRecordingProvider() { + const listeners = new Set(); + const sent = []; + return { + sent, + provider: { + postMessage(bytes) { + sent.push(bytes); + }, + subscribe(callback) { + listeners.add(callback); + return () => listeners.delete(callback); + }, + dispose() { + listeners.clear(); + }, + }, + deliver(message) { + for (const listener of [...listeners]) listener(message); + }, + }; +} + +test("createHostServer dispatches a request id to the matching entry and emits a response", async () => { + const requestId = 7; + const responseId = 8; + const { provider, sent, deliver } = makeRecordingProvider(); + + const entries = [ + { + kind: "request", + ids: { request: requestId, response: responseId }, + async handle(ctx, payload) { + assert.equal(typeof ctx.requestId, "string"); + // Echo with one extra byte so the test asserts that the right + // bytes flowed through. + const out = new Uint8Array(payload.length + 1); + out.set(payload); + out[payload.length] = 42; + return out; + }, + }, + ]; + + const server = createHostServer(provider, entries); + + const inboundFrame = encodeWireMessage({ + requestId: "req-1", + payload: { id: requestId, value: new Uint8Array([1, 2, 3]) }, + }); + assert.ok(inboundFrame.isOk(), "inbound frame must encode"); + deliver(inboundFrame.value); + + // Allow the handler microtask + send to resolve. + await new Promise((r) => setImmediate(r)); + + assert.equal(sent.length, 1, "exactly one response emitted"); + const decoded = decodeWireMessage(sent[0]); + assert.ok(decoded.isOk(), "response frame must decode"); + assert.equal(decoded.value.requestId, "req-1"); + assert.equal(decoded.value.payload.id, responseId); + assert.deepEqual( + Array.from(decoded.value.payload.value), + [1, 2, 3, 42], + "payload should echo + extra byte", + ); + + server.dispose(); +}); diff --git a/host-libs/js/shared/test/node-wasm-provider.test.mjs b/host-libs/js/shared/test/node-wasm-provider.test.mjs new file mode 100644 index 00000000..aab6f98e --- /dev/null +++ b/host-libs/js/shared/test/node-wasm-provider.test.mjs @@ -0,0 +1,59 @@ +// Smoke test that `createNodeWasmProvider` instantiates the WASM core, +// returns a usable `Provider`, and disposes cleanly without leaking +// resources back to the caller. + +import assert from "node:assert/strict"; +import test from "node:test"; + +import { createNodeWasmProvider } from "../dist/index.js"; + +function makeCallbacks() { + const unavailable = (name) => async () => { + throw new Error(`${name} unavailable`); + }; + const noopSubscribe = () => () => {}; + return { + navigateTo: async () => {}, + pushNotification: async () => {}, + devicePermission: async () => false, + remotePermission: async () => false, + featureSupported: async () => new Uint8Array(), + localStorageRead: async () => undefined, + localStorageWrite: async () => {}, + localStorageClear: async () => {}, + accountGet: unavailable("accountGet"), + accountGetAlias: unavailable("accountGetAlias"), + accountCreateProof: unavailable("accountCreateProof"), + getLegacyAccounts: unavailable("getLegacyAccounts"), + accountConnectionStatusSubscribe: noopSubscribe, + getUserId: unavailable("getUserId"), + signPayload: unavailable("signPayload"), + signRaw: unavailable("signRaw"), + statementStoreSubscribe: noopSubscribe, + statementStoreSubmit: unavailable("statementStoreSubmit"), + statementStoreCreateProof: unavailable("statementStoreCreateProof"), + preimageLookupSubscribe: noopSubscribe, + dispose: () => {}, + }; +} + +test("createNodeWasmProvider returns a usable Provider", async () => { + const provider = await createNodeWasmProvider(makeCallbacks()); + assert.equal(typeof provider.postMessage, "function"); + assert.equal(typeof provider.subscribe, "function"); + assert.equal(typeof provider.dispose, "function"); + + // Subscribe and immediately unsubscribe to exercise the listener + // bookkeeping without needing a valid frame. + const unsubscribe = provider.subscribe(() => {}); + unsubscribe(); + + provider.dispose(); +}); + +test("createNodeWasmProvider dispose is idempotent", async () => { + const provider = await createNodeWasmProvider(makeCallbacks()); + provider.dispose(); + // Second call must not throw. + provider.dispose(); +}); diff --git a/host-libs/js/shared/test/worker-protocol.test.mjs b/host-libs/js/shared/test/worker-protocol.test.mjs new file mode 100644 index 00000000..f1af135c --- /dev/null +++ b/host-libs/js/shared/test/worker-protocol.test.mjs @@ -0,0 +1,30 @@ +// Sanity test that the worker-protocol module is importable and exports +// what `createWebWorkerProvider` (from @parity/host-web) expects. The +// real web worker entry-point loads a browser-only WASM bundle, so we +// cannot boot it under Node; this test verifies the wire-shape of the +// shared protocol contract instead. + +import assert from "node:assert/strict"; +import test from "node:test"; + +import * as shared from "../dist/index.js"; +import * as workerProtocol from "../dist/worker-protocol.js"; + +test("worker-protocol module loads without runtime types (TS-only)", () => { + // The .js module compiles down to an empty body — assert that no + // runtime symbols are exported, since CallbackName / SubscriptionName + // / MainToWorker / WorkerToMain are type-only. + assert.deepEqual(Object.keys(workerProtocol), []); +}); + +test("@parity/host-shared exposes the documented surface", () => { + // Dispatcher re-export from @parity/truapi-host. + assert.equal(typeof shared.createHostServer, "function"); + assert.equal(typeof shared.toFlatResponsePayload, "function"); + assert.equal(typeof shared.toResponsePayload, "function"); + + // WASM provider helpers. + assert.equal(typeof shared.createWasmProvider, "function"); + assert.equal(typeof shared.createNodeWasmProvider, "function"); + assert.equal(typeof shared.createUnavailableCallbacks, "function"); +}); diff --git a/host-libs/js/shared/tsconfig.json b/host-libs/js/shared/tsconfig.json new file mode 100644 index 00000000..398a7002 --- /dev/null +++ b/host-libs/js/shared/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "lib": ["ES2022", "DOM", "WebWorker"] + }, + "include": ["src"] +} diff --git a/host-libs/js/web/.gitignore b/host-libs/js/web/.gitignore new file mode 100644 index 00000000..f4e2c6d6 --- /dev/null +++ b/host-libs/js/web/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.tsbuildinfo diff --git a/host-libs/js/web/README.md b/host-libs/js/web/README.md new file mode 100644 index 00000000..543ffbb0 --- /dev/null +++ b/host-libs/js/web/README.md @@ -0,0 +1,49 @@ +# @parity/host-web + +Browser TrUAPI host wrapper: + +- `createIframeHost` — embed a product iframe, transfer a `MessagePort` + into it via the `truapi-init` handshake, and hand the host-side port + back to the caller. +- `createWebWorkerProvider` — spawn the truapi-server WASM core inside a + Web Worker and bridge it into a `Provider` (so smoldot, header + verification, and dispatcher work run off the page main thread). + +## Example + +```ts +import { createMessagePortProvider, createTransport } from "@parity/truapi"; +import { createHostServer } from "@parity/truapi-host"; +import HostWorker from "@parity/host-shared/dist/worker-runtime.js?worker"; +import { createIframeHost, createWebWorkerProvider } from "@parity/host-web"; + +// 1. WASM core inside a Worker, exposed as a Provider. +const coreProvider = await createWebWorkerProvider(new HostWorker(), { + navigateTo: async (url) => window.open(url, "_blank"), + pushNotification: async () => {}, + devicePermission: async () => true, + remotePermission: async () => true, + featureSupported: async (payload) => payload, + localStorageRead: async () => undefined, + localStorageWrite: async () => {}, + localStorageClear: async () => {}, + /* ...remaining account / signing / store callbacks */ +} as never); + +// 2. Wire the iframe's MessageChannel into the same provider. +createIframeHost({ + iframeUrl: "http://localhost:5174", + container: document.getElementById("app")!, + onPort: (port) => { + const iframeProvider = createMessagePortProvider(port); + // hand both providers off to your host server / transport pair + createHostServer(iframeProvider, [ + /* dispatch entries */ + ]); + }, +}); +``` + +The window-level legacy `postMessage` fallback present in earlier +prototypes is intentionally not provided here; products must use the +canonical MessageChannel rail. diff --git a/host-libs/js/web/package-lock.json b/host-libs/js/web/package-lock.json new file mode 100644 index 00000000..da7d5556 --- /dev/null +++ b/host-libs/js/web/package-lock.json @@ -0,0 +1,66 @@ +{ + "name": "@parity/host-web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@parity/host-web", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@parity/host-shared": "file:../shared", + "@parity/truapi": "file:../../../js/packages/truapi" + }, + "devDependencies": { + "typescript": "^5.7" + } + }, + "../../../js/packages/truapi": { + "name": "@parity/truapi", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "neverthrow": "^8.2.0", + "scale-ts": "^1.6.1" + }, + "devDependencies": { + "typescript": "^5.7" + } + }, + "../shared": { + "name": "@parity/host-shared", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@parity/truapi": "file:../../../js/packages/truapi", + "@parity/truapi-host": "file:../../../js/packages/truapi-host" + }, + "devDependencies": { + "typescript": "^5.7" + } + }, + "node_modules/@parity/host-shared": { + "resolved": "../shared", + "link": true + }, + "node_modules/@parity/truapi": { + "resolved": "../../../js/packages/truapi", + "link": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/host-libs/js/web/package.json b/host-libs/js/web/package.json new file mode 100644 index 00000000..594d201f --- /dev/null +++ b/host-libs/js/web/package.json @@ -0,0 +1,32 @@ +{ + "name": "@parity/host-web", + "version": "0.1.0", + "description": "Browser TrUAPI host wrapper: iframe embedding and Web Worker-backed WASM provider", + "license": "MIT", + "author": "Parity Technologies ", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "tsc", + "prepare": "npm run build", + "test": "for f in test/*.test.mjs; do echo \"=== $f ===\" && node --test \"$f\" || exit 1; done" + }, + "dependencies": { + "@parity/host-shared": "file:../shared", + "@parity/truapi": "file:../../../js/packages/truapi" + }, + "devDependencies": { + "typescript": "^5.7" + } +} diff --git a/host-libs/js/web/src/create-iframe-host.ts b/host-libs/js/web/src/create-iframe-host.ts new file mode 100644 index 00000000..724dc5b5 --- /dev/null +++ b/host-libs/js/web/src/create-iframe-host.ts @@ -0,0 +1,140 @@ +import type { Provider } from "@parity/truapi"; + +/** + * Options for `createIframeHost`. + */ +export interface IframeHostOptions { + /** URL of the product iframe. */ + iframeUrl: string; + /** Container element the iframe is appended to. */ + container: HTMLElement; + /** + * Called with one end of the MessageChannel once the iframe has loaded. + * Hosts typically pipe this into a `Provider` (e.g. via + * `createMessagePortProvider` from `@parity/truapi`) and hand the + * provider to `createHostServer`. + */ + onPort: (port: MessagePort) => void; + /** + * Optional explicit allow-list origin. Defaults to the origin of + * `iframeUrl`. Throws if it disagrees with the iframe URL's origin. + */ + allowedOrigin?: string; + /** Override the default iframe sandbox attribute. */ + sandbox?: string; +} + +/** + * Handle returned by `createIframeHost`. + */ +export interface IframeHost { + iframe: HTMLIFrameElement; + dispose: () => void; +} + +const DEFAULT_IFRAME_SANDBOX = "allow-forms allow-same-origin allow-scripts"; + +function resolveAllowedOrigin( + iframeUrl: string, + allowedOrigin?: string, +): string { + const targetUrl = new URL(iframeUrl, window.location.href); + if (targetUrl.protocol !== "http:" && targetUrl.protocol !== "https:") { + throw new Error( + `Iframe host only allows http(s) playground URLs, received ${targetUrl.protocol}`, + ); + } + + if (!allowedOrigin) { + return targetUrl.origin; + } + + const normalizedOrigin = new URL(allowedOrigin).origin; + if (normalizedOrigin !== targetUrl.origin) { + throw new Error( + `Iframe host origin policy mismatch, expected ${normalizedOrigin}, got ${targetUrl.origin}`, + ); + } + + return normalizedOrigin; +} + +/** + * Embed a product iframe and transfer a `MessagePort` into it. The host + * keeps the other end and passes it to a `Provider` (typically via + * `createMessagePortProvider`). All product traffic flows over the + * MessageChannel — there is no `window.postMessage` legacy fallback. + */ +export function createIframeHost(options: IframeHostOptions): IframeHost { + const { + iframeUrl, + container, + onPort, + allowedOrigin, + sandbox = DEFAULT_IFRAME_SANDBOX, + } = options; + + const channel = new MessageChannel(); + const hostPort = channel.port1; + const productPort = channel.port2; + const targetOrigin = resolveAllowedOrigin(iframeUrl, allowedOrigin); + + // Hand the host-side port to the caller immediately so it can wire up + // a provider before the iframe finishes loading. Queued postMessage + // calls are delivered once the channel is started by the provider. + onPort(hostPort); + + const iframe = document.createElement("iframe"); + iframe.style.width = "100%"; + iframe.style.height = "100%"; + iframe.style.border = "none"; + iframe.src = iframeUrl; + iframe.setAttribute("sandbox", sandbox); + iframe.referrerPolicy = "no-referrer"; + + let initSent = false; + const sendInit = (): void => { + if (initSent) return; + const contentWindow = iframe.contentWindow; + if (!contentWindow) return; + initSent = true; + contentWindow.postMessage({ type: "truapi-init" }, targetOrigin, [ + productPort, + ]); + }; + + iframe.addEventListener("load", sendInit); + + const onWindowMessage = (event: MessageEvent): void => { + if (event.source !== iframe.contentWindow) return; + if (event.origin !== targetOrigin) return; + if (event.data?.type === "truapi-playground-ready") { + sendInit(); + } + }; + window.addEventListener("message", onWindowMessage); + + container.appendChild(iframe); + + return { + iframe, + dispose() { + window.removeEventListener("message", onWindowMessage); + try { + hostPort.close(); + } catch { + // already closed + } + try { + productPort.close(); + } catch { + // already closed + } + iframe.remove(); + }, + }; +} + +// Suppress unused-symbol warning when consumers do not import Provider +// directly; declaring the type relationship keeps the contract visible. +export type { Provider }; diff --git a/host-libs/js/web/src/create-worker-host-runtime.ts b/host-libs/js/web/src/create-worker-host-runtime.ts new file mode 100644 index 00000000..9ff9a617 --- /dev/null +++ b/host-libs/js/web/src/create-worker-host-runtime.ts @@ -0,0 +1,370 @@ +import type { Provider } from "@parity/truapi"; +import type { + CallbackName, + ChainConnection, + MainToWorker, + SubscriptionName, + WasmRawCallbacks, + WorkerToMain, +} from "@parity/host-shared"; + +interface WorkerProviderState { + worker: Worker; + rawCallbacks: WasmRawCallbacks; + listeners: Set<(message: Uint8Array) => void>; + closeListeners: Set<(error: Error) => void>; + subscriptionDisposers: Map void>; + chainConnections: Map; + disposed: boolean; +} + +function errMsg(err: unknown): string { + if (err instanceof Error) return err.message; + if (typeof err === "string") return err; + return JSON.stringify(err); +} + +function handleCallbackRequest( + state: WorkerProviderState, + msg: { + requestId: number; + name: CallbackName; + args: readonly unknown[]; + }, +): void { + const fn = ( + state.rawCallbacks as unknown as Record< + string, + (...args: readonly unknown[]) => unknown + > + )[msg.name]; + if (!fn) { + const reply: MainToWorker = { + kind: "callbackResponse", + requestId: msg.requestId, + ok: false, + error: `unknown callback: ${msg.name}`, + }; + state.worker.postMessage(reply); + return; + } + Promise.resolve() + .then(() => fn(...msg.args)) + .then( + (value) => { + const reply: MainToWorker = { + kind: "callbackResponse", + requestId: msg.requestId, + ok: true, + value, + }; + state.worker.postMessage(reply); + }, + (err) => { + const reply: MainToWorker = { + kind: "callbackResponse", + requestId: msg.requestId, + ok: false, + error: errMsg(err), + }; + state.worker.postMessage(reply); + }, + ); +} + +function handleSubscriptionStart( + state: WorkerProviderState, + msg: { + subId: number; + name: SubscriptionName; + payload: Uint8Array | null; + }, +): void { + const sendItem = (bytes: Uint8Array): void => { + if (state.disposed) return; + const post: MainToWorker = { + kind: "subscriptionItem", + subId: msg.subId, + bytes, + }; + state.worker.postMessage(post); + }; + let dispose: unknown; + try { + if (msg.name === "accountConnectionStatusSubscribe") { + dispose = state.rawCallbacks.accountConnectionStatusSubscribe(sendItem); + } else if (msg.payload !== null) { + const fn = + msg.name === "statementStoreSubscribe" + ? state.rawCallbacks.statementStoreSubscribe + : state.rawCallbacks.preimageLookupSubscribe; + dispose = fn(msg.payload, sendItem); + } else { + console.warn( + `[truapi worker] ${msg.name} requires payload, none received`, + ); + return; + } + } catch (err) { + console.error(`[truapi worker] ${msg.name} threw on start:`, err); + return; + } + if (typeof dispose === "function") { + state.subscriptionDisposers.set(msg.subId, dispose as () => void); + } +} + +function handleSubscriptionStop( + state: WorkerProviderState, + msg: { subId: number }, +): void { + const dispose = state.subscriptionDisposers.get(msg.subId); + if (!dispose) return; + state.subscriptionDisposers.delete(msg.subId); + try { + dispose(); + } catch (err) { + console.warn("[truapi worker] subscription dispose threw:", err); + } +} + +async function handleChainConnectStart( + state: WorkerProviderState, + msg: { connId: number; genesisHash: string }, +): Promise { + const chainConnect = state.rawCallbacks.chainConnect; + if (!chainConnect) { + const reply: MainToWorker = { + kind: "chainConnectAck", + connId: msg.connId, + ok: false, + error: "host did not supply chainConnect", + }; + state.worker.postMessage(reply); + return; + } + const onResponse = (json: string): void => { + if (state.disposed) return; + const post: MainToWorker = { + kind: "chainResponse", + connId: msg.connId, + json, + }; + state.worker.postMessage(post); + }; + try { + const conn = await chainConnect(msg.genesisHash, onResponse); + if (!conn) { + const reply: MainToWorker = { + kind: "chainConnectAck", + connId: msg.connId, + ok: false, + error: `chainConnect returned null for genesisHash ${msg.genesisHash}`, + }; + state.worker.postMessage(reply); + return; + } + state.chainConnections.set(msg.connId, conn); + const reply: MainToWorker = { + kind: "chainConnectAck", + connId: msg.connId, + ok: true, + }; + state.worker.postMessage(reply); + } catch (err) { + const reply: MainToWorker = { + kind: "chainConnectAck", + connId: msg.connId, + ok: false, + error: errMsg(err), + }; + state.worker.postMessage(reply); + } +} + +function handleChainSend( + state: WorkerProviderState, + msg: { connId: number; request: string }, +): void { + const conn = state.chainConnections.get(msg.connId); + if (!conn) return; + try { + conn.send(msg.request); + } catch (err) { + console.warn("[truapi worker] chain send threw:", err); + } +} + +function handleChainClose( + state: WorkerProviderState, + msg: { connId: number }, +): void { + const conn = state.chainConnections.get(msg.connId); + if (!conn) return; + state.chainConnections.delete(msg.connId); + try { + conn.close(); + } catch (err) { + console.warn("[truapi worker] chain close threw:", err); + } +} + +export interface CreateWebWorkerProviderOptions { + /** Toggle the wasm core's debug logging. Default: `false`. */ + debug?: boolean; +} + +/** + * Spawn the truapi-server WASM in `worker` and bridge it into a + * `Provider`. The provider can be handed to `createHostServer` from + * `@parity/truapi-host`. + * + * The caller is responsible for instantiating the Worker — Vite users + * typically import the worker entry-point with `?worker`: + * + * ```ts + * import HostWorker from "@parity/host-shared/dist/worker-runtime.js?worker"; + * const worker = new HostWorker(); + * const provider = await createWebWorkerProvider(worker, callbacks); + * ``` + * + * Resolves once the worker reports `ready` and rejects if the WASM + * fails to load. + */ +export function createWebWorkerProvider( + worker: Worker, + callbacks: Omit, + options: CreateWebWorkerProviderOptions = {}, +): Promise { + return new Promise((resolve, reject) => { + const state: WorkerProviderState = { + worker, + // `emitFrame` is satisfied by the worker side; main thread never + // calls it. Fill in a no-op so the typed callback set is complete. + rawCallbacks: { + ...(callbacks as WasmRawCallbacks), + emitFrame: () => {}, + }, + listeners: new Set(), + closeListeners: new Set(), + subscriptionDisposers: new Map(), + chainConnections: new Map(), + disposed: false, + }; + + const onMessage = (ev: MessageEvent): void => { + const msg = ev.data; + switch (msg.kind) { + case "ready": + break; + case "error": + console.error("[truapi worker]", msg.error); + for (const listener of [...state.closeListeners]) + listener(new Error(msg.error)); + break; + case "frame": + for (const listener of [...state.listeners]) listener(msg.bytes); + break; + case "callbackRequest": + handleCallbackRequest(state, msg); + break; + case "subscriptionStart": + handleSubscriptionStart(state, msg); + break; + case "subscriptionStop": + handleSubscriptionStop(state, msg); + break; + case "chainConnectStart": + void handleChainConnectStart(state, msg); + break; + case "chainSend": + handleChainSend(state, msg); + break; + case "chainClose": + handleChainClose(state, msg); + break; + } + }; + + const onError = (e: ErrorEvent): void => { + cleanupInit(); + reject(new Error(`worker init failed: ${e.message}`)); + }; + + const onInitMessage = (ev: MessageEvent): void => { + const msg = ev.data; + if (msg.kind === "ready") { + cleanupInit(); + const configure: MainToWorker = { + kind: "configure", + debug: options.debug ?? false, + }; + worker.postMessage(configure); + worker.addEventListener("message", onMessage); + resolve(buildProvider(state)); + } else if (msg.kind === "error") { + cleanupInit(); + reject(new Error(`worker init reported error: ${msg.error}`)); + } + }; + + const cleanupInit = (): void => { + worker.removeEventListener("error", onError); + worker.removeEventListener("message", onInitMessage); + }; + + worker.addEventListener("error", onError); + worker.addEventListener("message", onInitMessage); + }); +} + +function buildProvider(state: WorkerProviderState): Provider { + return { + postMessage(bytes: Uint8Array): void { + if (state.disposed) return; + const post: MainToWorker = { kind: "frame", bytes }; + state.worker.postMessage(post); + }, + subscribe(callback) { + state.listeners.add(callback); + return () => { + state.listeners.delete(callback); + }; + }, + subscribeClose(callback) { + state.closeListeners.add(callback); + return () => { + state.closeListeners.delete(callback); + }; + }, + dispose() { + if (state.disposed) return; + state.disposed = true; + for (const fn of state.subscriptionDisposers.values()) { + try { + fn(); + } catch { + // ignore during teardown + } + } + state.subscriptionDisposers.clear(); + for (const conn of state.chainConnections.values()) { + try { + conn.close(); + } catch { + // ignore during teardown + } + } + state.chainConnections.clear(); + try { + const post: MainToWorker = { kind: "dispose" }; + state.worker.postMessage(post); + } catch { + // ignore if worker already gone + } + state.worker.terminate(); + state.listeners.clear(); + state.closeListeners.clear(); + }, + }; +} diff --git a/host-libs/js/web/src/index.ts b/host-libs/js/web/src/index.ts new file mode 100644 index 00000000..6876c6cc --- /dev/null +++ b/host-libs/js/web/src/index.ts @@ -0,0 +1,4 @@ +export type { IframeHost, IframeHostOptions } from "./create-iframe-host.js"; +export { createIframeHost } from "./create-iframe-host.js"; +export type { CreateWebWorkerProviderOptions } from "./create-worker-host-runtime.js"; +export { createWebWorkerProvider } from "./create-worker-host-runtime.js"; diff --git a/host-libs/js/web/test/create-iframe-host.test.mjs b/host-libs/js/web/test/create-iframe-host.test.mjs new file mode 100644 index 00000000..d237b986 --- /dev/null +++ b/host-libs/js/web/test/create-iframe-host.test.mjs @@ -0,0 +1,115 @@ +// Verify that `createIframeHost` hands a MessagePort back through `onPort`, +// constructs an iframe with the expected attributes, and posts the +// `truapi-init` handshake to the iframe's contentWindow on load. + +import assert from "node:assert/strict"; +import test from "node:test"; + +import { createIframeHost } from "../dist/index.js"; + +function setupFakeDom() { + // Track listeners on the synthetic `window` and the iframe so the + // test can simulate the iframe `load` event after construction. + const iframeListeners = new Map(); + const windowListeners = new Map(); + const contentPostMessage = test.mock.fn(); + + const iframe = { + style: {}, + setAttribute: test.mock.fn(), + addEventListener: (name, fn) => { + iframeListeners.set(name, fn); + }, + removeEventListener: () => {}, + remove: test.mock.fn(), + referrerPolicy: "", + src: "", + contentWindow: { + postMessage: contentPostMessage, + }, + }; + + const container = { + appendChild: test.mock.fn(), + }; + + globalThis.document = { + createElement: (tag) => { + assert.equal(tag, "iframe"); + return iframe; + }, + }; + globalThis.window = { + location: { href: "http://localhost:5174/" }, + addEventListener: (name, fn) => { + windowListeners.set(name, fn); + }, + removeEventListener: () => {}, + }; + + return { iframe, container, contentPostMessage, iframeListeners }; +} + +function teardownFakeDom() { + delete globalThis.document; + delete globalThis.window; +} + +test("createIframeHost hands back a MessagePort and posts truapi-init on load", () => { + const { iframe, container, contentPostMessage, iframeListeners } = + setupFakeDom(); + + try { + let receivedPort = null; + const host = createIframeHost({ + iframeUrl: "http://localhost:5174/", + container, + onPort: (port) => { + receivedPort = port; + }, + }); + + assert.ok(receivedPort, "onPort must fire synchronously"); + assert.equal(typeof receivedPort.postMessage, "function"); + assert.equal(container.appendChild.mock.callCount(), 1); + assert.equal(host.iframe, iframe); + assert.equal(iframe.src, "http://localhost:5174/"); + + // Simulate the iframe finishing load. + const onLoad = iframeListeners.get("load"); + assert.ok(onLoad, "load handler must be registered"); + onLoad(); + + assert.equal(contentPostMessage.mock.callCount(), 1); + const [body, origin, transferList] = contentPostMessage.mock.calls[0].arguments; + assert.deepEqual(body, { type: "truapi-init" }); + assert.equal(origin, "http://localhost:5174"); + assert.equal(transferList.length, 1); + + // Idempotent — a second load event must not send another init. + onLoad(); + assert.equal(contentPostMessage.mock.callCount(), 1); + + host.dispose(); + assert.equal(iframe.remove.mock.callCount(), 1); + } finally { + teardownFakeDom(); + } +}); + +test("createIframeHost rejects non-http(s) iframe URLs", () => { + setupFakeDom(); + try { + assert.throws( + () => + createIframeHost({ + iframeUrl: "file:///etc/passwd", + container: { appendChild: () => {} }, + onPort: () => {}, + }), + /only allows http\(s\)/, + ); + } finally { + teardownFakeDom(); + } +}); diff --git a/host-libs/js/web/tsconfig.json b/host-libs/js/web/tsconfig.json new file mode 100644 index 00000000..033ca333 --- /dev/null +++ b/host-libs/js/web/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "lib": ["ES2022", "DOM"] + }, + "include": ["src"] +} From 32daaa1109c9c3eca8ca4104e9d1c6a16c667592 Mon Sep 17 00:00:00 2001 From: pgherveou Date: Sun, 17 May 2026 09:57:39 +0000 Subject: [PATCH 07/49] feat(host-libs): android + ios native shells (Phase 6 native, #103) Two thin language-idiomatic SDKs wrapping the UniFFI bindings emitted from truapi-server, ready to be consumed by iOS/Android host repos. host-libs/android/ (Kotlin): - io.parity.truapi.{TrUAPIHostCore, HostBridge, HostStorage, CoreInbound, WebViewTransport} wrapping NativeTrUApiCore via UniFFI - build.gradle.kts (Android library, JDK 17, JNA 5.14, kotlinx-coroutines) - AndroidManifest, settings.gradle.kts, gradle.properties for standalone assembly; also includable as :host-libs:android from a parent project - Generated UniFFI bindings under src/main/kotlin/generated/ host-libs/ios/TrUAPIHost/ (Swift Package): - TrUAPIHost.{TrUAPIHostCore, HostBridge, HostStorageBackend, CoreInbound, WebViewTransport, LocalhostBridgeBootstrap} - Package.swift exposing TrUAPIHost + truapi_serverFFI systemLibrary targets - Generated UniFFI bindings under Sources/TrUAPIHost/truapi_server.swift and Sources/truapi_serverFFI/include/{truapi_serverFFI.h,module.modulemap} cdylib built with --features ws-bridge so the native surface includes startWsBridge/stopWsBridge for WebView hosts that prefer the localhost WS rail. HostBridge interface keeps the v0.1 device/remote permission split as two named methods (no merged prompt_permission). Bindings committed (consumers don't run Rust). 92K Kotlin (2467 LOC), 56K Swift (1744 LOC), 36K C header. Relates to #96, #103. --- host-libs/android/.gitignore | 5 + host-libs/android/README.md | 115 + host-libs/android/build.gradle.kts | 46 + host-libs/android/gradle.properties | 3 + host-libs/android/settings.gradle.kts | 21 + .../android/src/main/AndroidManifest.xml | 2 + .../uniffi/truapi_server/truapi_server.kt | 2467 +++++++++++++++++ .../kotlin/io/parity/truapi/TrUAPIHost.kt | 267 ++ host-libs/ios/TrUAPIHost/.gitignore | 4 + host-libs/ios/TrUAPIHost/Package.swift | 35 + host-libs/ios/TrUAPIHost/README.md | 125 + .../Sources/TrUAPIHost/TrUAPIHost.swift | 306 ++ .../Sources/TrUAPIHost/truapi_server.swift | 1745 ++++++++++++ .../truapi_serverFFI/include/module.modulemap | 7 + .../include/truapi_serverFFI.h | 779 ++++++ 15 files changed, 5927 insertions(+) create mode 100644 host-libs/android/.gitignore create mode 100644 host-libs/android/README.md create mode 100644 host-libs/android/build.gradle.kts create mode 100644 host-libs/android/gradle.properties create mode 100644 host-libs/android/settings.gradle.kts create mode 100644 host-libs/android/src/main/AndroidManifest.xml create mode 100644 host-libs/android/src/main/kotlin/generated/uniffi/truapi_server/truapi_server.kt create mode 100644 host-libs/android/src/main/kotlin/io/parity/truapi/TrUAPIHost.kt create mode 100644 host-libs/ios/TrUAPIHost/.gitignore create mode 100644 host-libs/ios/TrUAPIHost/Package.swift create mode 100644 host-libs/ios/TrUAPIHost/README.md create mode 100644 host-libs/ios/TrUAPIHost/Sources/TrUAPIHost/TrUAPIHost.swift create mode 100644 host-libs/ios/TrUAPIHost/Sources/TrUAPIHost/truapi_server.swift create mode 100644 host-libs/ios/TrUAPIHost/Sources/truapi_serverFFI/include/module.modulemap create mode 100644 host-libs/ios/TrUAPIHost/Sources/truapi_serverFFI/include/truapi_serverFFI.h diff --git a/host-libs/android/.gitignore b/host-libs/android/.gitignore new file mode 100644 index 00000000..98778d9e --- /dev/null +++ b/host-libs/android/.gitignore @@ -0,0 +1,5 @@ +.gradle/ +build/ +local.properties +*.iml +.idea/ diff --git a/host-libs/android/README.md b/host-libs/android/README.md new file mode 100644 index 00000000..08636002 --- /dev/null +++ b/host-libs/android/README.md @@ -0,0 +1,115 @@ +# TrUAPI Android host adapter + +*Thin Kotlin shell over the Rust TrUAPI core (UniFFI) plus an Android `WebView` byte transport. Wire decoding, request routing, and subscription lifecycle stay in the Rust core.* + +## What this package is for + +The public surface lives in [`src/main/kotlin/io/parity/truapi/TrUAPIHost.kt`](src/main/kotlin/io/parity/truapi/TrUAPIHost.kt): + +- `HostBridge` - callback bundle the embedding app implements. Split into device permissions, remote permissions, navigation, push, feature support, and scoped storage. +- `TrUAPIHostCore` - owning wrapper around the UniFFI-generated `NativeTrUApiCore`. Implements `CoreInbound`, owns the bridge lifetime, exposes session and WS bridge controls. +- `WebViewTransport` - base64-over-`JavascriptInterface` byte pipe between a `WebView` and any `CoreInbound`. Injects a `window.trUApi` shim that matches the JS host adapter shape. +- `bootstrapScript` - the JS shim, exposed so apps can inject it through their own WebView bootstrap path. + +The generated UniFFI bindings live under `src/main/kotlin/generated/uniffi/truapi_server/`. They are committed (they're large and consumers should not need a Rust toolchain). + +## Architecture + +```text +product app in WebView + Uint8Array frames via window.trUApi + | + v +WebViewTransport + base64 over Android JS bridge + | + v +TrUAPIHostCore (CoreInbound) + → uniffi → libtruapi_server.so +``` + +Inbound flow: + +1. Product JS calls `window.trUApi.postMessage(bytes)` +2. `WebViewTransport` receives base64 through `@JavascriptInterface` +3. `TrUAPIHostCore.receiveFromProduct(...)` forwards bytes into the Rust dispatcher +4. The Rust core emits a response frame; `HostBridge.onCoreResponse(...)` fires +5. The embedder typically pumps the response back through `WebViewTransport.sendToProduct(...)`, which calls `window.__trUApiReceive(...)` + +The Rust core also calls `HostBridge` directly for platform capabilities: `navigateTo`, `pushNotification`, `devicePermission`, `remotePermission`, `featureSupported`, and the `storage` slot. + +## Permissions split + +The core's `Permissions` platform trait has two methods, and so does the bridge: + +- `devicePermission(request)` - OS-scoped grants (camera, mic, location, push). `request` is a SCALE-encoded `v01::HostDevicePermissionRequest`. +- `remotePermission(request)` - per-product capability bundles. `request` is a SCALE-encoded `v01::RemotePermissionRequest`. + +Both return a `Boolean` granted flag. SCALE decoding for the UI prompt is done by the `@parity/truapi` JS client (or any consumer that links the protocol crate's types directly). + +## Example + +```kt +import android.webkit.WebView +import io.parity.truapi.HostBridge +import io.parity.truapi.HostStorage +import io.parity.truapi.TrUAPIHostCore +import io.parity.truapi.WebViewTransport +import uniffi.truapi_server.HostNavigateRejection +import uniffi.truapi_server.HostRejection + +class MyStorage : HostStorage { + private val map = mutableMapOf() + override fun read(key: String) = map[key] + override fun write(key: String, value: ByteArray) { map[key] = value } + override fun clear(key: String) { map.remove(key) } +} + +class MyBridge(private val transport: WebViewTransport) : HostBridge { + override val storage = MyStorage() + override fun onCoreResponse(frame: ByteArray) = transport.sendToProduct(frame) + override fun navigateTo(url: String) { /* open in browser */ } + override fun pushNotification(payload: ByteArray) { /* show notification */ } + override fun devicePermission(request: ByteArray): Boolean = TODO("prompt user") + override fun remotePermission(request: ByteArray): Boolean = TODO("prompt user") + override fun featureSupported(request: ByteArray): Boolean = false +} + +val webView: WebView = existingWebView +lateinit var transport: WebViewTransport +val bridge = MyBridge(transport = WebViewTransport(webView, core = object : io.parity.truapi.CoreInbound { + override fun receiveFromProduct(frame: ByteArray) = core.receiveFromProduct(frame) +}).also { transport = it }) +val core = TrUAPIHostCore(bridge) +transport.attach() +``` + +(In practice, build the `TrUAPIHostCore` first, hand it to a `WebViewTransport`, and have the bridge close back over the same transport instance.) + +## Loading the cdylib + +JNA looks for `libtruapi_server.so` in the standard `jniLibs` paths. Bundle the per-ABI builds under: + +``` +src/main/jniLibs/arm64-v8a/libtruapi_server.so +src/main/jniLibs/armeabi-v7a/libtruapi_server.so +src/main/jniLibs/x86_64/libtruapi_server.so +``` + +Build the cdylib with the `ws-bridge` feature if you want `startWsBridge` to be functional: + +``` +cargo build -p truapi-server --release --features ws-bridge --target +``` + +## Regenerating the bindings + +The committed bindings under `src/main/kotlin/generated/uniffi/` are produced from the workspace `uniffi-bindgen-cli`: + +```bash +cargo build -p truapi-server --release --features ws-bridge +cargo run -p uniffi-bindgen-cli -- generate \ + --library target/release/libtruapi_server.so \ + --language kotlin \ + --out-dir host-libs/android/src/main/kotlin/generated +``` diff --git a/host-libs/android/build.gradle.kts b/host-libs/android/build.gradle.kts new file mode 100644 index 00000000..b7cfd336 --- /dev/null +++ b/host-libs/android/build.gradle.kts @@ -0,0 +1,46 @@ +// TrUAPI Android host adapter. +// +// Wraps the UniFFI-generated bindings in `src/main/kotlin/generated/uniffi/` +// behind a thin Kotlin API and provides a WebView byte transport for the +// product page. The Rust core (compiled to libtruapi_server.so) handles all +// wire decoding, routing, subscription lifecycle, and host capability +// dispatch. + +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "io.parity.truapi" + compileSdk = 34 + + defaultConfig { + minSdk = 26 + } + + sourceSets { + getByName("main") { + java.srcDirs("src/main/kotlin") + manifest.srcFile("src/main/AndroidManifest.xml") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } +} + +dependencies { + // UniFFI Kotlin bindings use JNA for FFI. + implementation("net.java.dev.jna:jna:5.14.0@aar") + // The generated callback adapter wraps user-supplied lambdas; coroutines + // are not required by the bindings themselves but are commonly used by + // consumers when dispatching on a background scope. + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1") +} diff --git a/host-libs/android/gradle.properties b/host-libs/android/gradle.properties new file mode 100644 index 00000000..84b4d6bb --- /dev/null +++ b/host-libs/android/gradle.properties @@ -0,0 +1,3 @@ +android.useAndroidX=true +kotlin.code.style=official +org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 diff --git a/host-libs/android/settings.gradle.kts b/host-libs/android/settings.gradle.kts new file mode 100644 index 00000000..6e378a3b --- /dev/null +++ b/host-libs/android/settings.gradle.kts @@ -0,0 +1,21 @@ +// Standalone settings so this directory can be assembled in isolation. +// When consumed from a host app, prefer including it via the host's +// `settings.gradle.kts` (e.g. `include(":host-libs:android")`). + +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "truapi-host-android" diff --git a/host-libs/android/src/main/AndroidManifest.xml b/host-libs/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8072ee00 --- /dev/null +++ b/host-libs/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/host-libs/android/src/main/kotlin/generated/uniffi/truapi_server/truapi_server.kt b/host-libs/android/src/main/kotlin/generated/uniffi/truapi_server/truapi_server.kt new file mode 100644 index 00000000..62717abb --- /dev/null +++ b/host-libs/android/src/main/kotlin/generated/uniffi/truapi_server/truapi_server.kt @@ -0,0 +1,2467 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +@file:Suppress("NAME_SHADOWING") + +package uniffi.truapi_server + +// Common helper code. +// +// Ideally this would live in a separate .kt file where it can be unittested etc +// in isolation, and perhaps even published as a re-useable package. +// +// However, it's important that the details of how this helper code works (e.g. the +// way that different builtin types are passed across the FFI) exactly match what's +// expected by the Rust code on the other side of the interface. In practice right +// now that means coming from the exact some version of `uniffi` that was used to +// compile the Rust component. The easiest way to ensure this is to bundle the Kotlin +// helpers directly inline like we're doing here. + +import com.sun.jna.Library +import com.sun.jna.IntegerType +import com.sun.jna.Native +import com.sun.jna.Pointer +import com.sun.jna.Structure +import com.sun.jna.Callback +import com.sun.jna.ptr.* +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.CharBuffer +import java.nio.charset.CodingErrorAction +import java.util.concurrent.atomic.AtomicLong +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean + +// This is a helper for safely working with byte buffers returned from the Rust code. +// A rust-owned buffer is represented by its capacity, its current length, and a +// pointer to the underlying data. + +/** + * @suppress + */ +@Structure.FieldOrder("capacity", "len", "data") +open class RustBuffer : Structure() { + // Note: `capacity` and `len` are actually `ULong` values, but JVM only supports signed values. + // When dealing with these fields, make sure to call `toULong()`. + @JvmField var capacity: Long = 0 + @JvmField var len: Long = 0 + @JvmField var data: Pointer? = null + + class ByValue: RustBuffer(), Structure.ByValue + class ByReference: RustBuffer(), Structure.ByReference + + internal fun setValue(other: RustBuffer) { + capacity = other.capacity + len = other.len + data = other.data + } + + companion object { + internal fun alloc(size: ULong = 0UL) = uniffiRustCall() { status -> + // Note: need to convert the size to a `Long` value to make this work with JVM. + UniffiLib.INSTANCE.ffi_truapi_server_rustbuffer_alloc(size.toLong(), status) + }.also { + if(it.data == null) { + throw RuntimeException("RustBuffer.alloc() returned null data pointer (size=${size})") + } + } + + internal fun create(capacity: ULong, len: ULong, data: Pointer?): RustBuffer.ByValue { + var buf = RustBuffer.ByValue() + buf.capacity = capacity.toLong() + buf.len = len.toLong() + buf.data = data + return buf + } + + internal fun free(buf: RustBuffer.ByValue) = uniffiRustCall() { status -> + UniffiLib.INSTANCE.ffi_truapi_server_rustbuffer_free(buf, status) + } + } + + @Suppress("TooGenericExceptionThrown") + fun asByteBuffer() = + this.data?.getByteBuffer(0, this.len.toLong())?.also { + it.order(ByteOrder.BIG_ENDIAN) + } +} + +/** + * The equivalent of the `*mut RustBuffer` type. + * Required for callbacks taking in an out pointer. + * + * Size is the sum of all values in the struct. + * + * @suppress + */ +class RustBufferByReference : ByReference(16) { + /** + * Set the pointed-to `RustBuffer` to the given value. + */ + fun setValue(value: RustBuffer.ByValue) { + // NOTE: The offsets are as they are in the C-like struct. + val pointer = getPointer() + pointer.setLong(0, value.capacity) + pointer.setLong(8, value.len) + pointer.setPointer(16, value.data) + } + + /** + * Get a `RustBuffer.ByValue` from this reference. + */ + fun getValue(): RustBuffer.ByValue { + val pointer = getPointer() + val value = RustBuffer.ByValue() + value.writeField("capacity", pointer.getLong(0)) + value.writeField("len", pointer.getLong(8)) + value.writeField("data", pointer.getLong(16)) + + return value + } +} + +// This is a helper for safely passing byte references into the rust code. +// It's not actually used at the moment, because there aren't many things that you +// can take a direct pointer to in the JVM, and if we're going to copy something +// then we might as well copy it into a `RustBuffer`. But it's here for API +// completeness. + +@Structure.FieldOrder("len", "data") +internal open class ForeignBytes : Structure() { + @JvmField var len: Int = 0 + @JvmField var data: Pointer? = null + + class ByValue : ForeignBytes(), Structure.ByValue +} +/** + * The FfiConverter interface handles converter types to and from the FFI + * + * All implementing objects should be public to support external types. When a + * type is external we need to import it's FfiConverter. + * + * @suppress + */ +public interface FfiConverter { + // Convert an FFI type to a Kotlin type + fun lift(value: FfiType): KotlinType + + // Convert an Kotlin type to an FFI type + fun lower(value: KotlinType): FfiType + + // Read a Kotlin type from a `ByteBuffer` + fun read(buf: ByteBuffer): KotlinType + + // Calculate bytes to allocate when creating a `RustBuffer` + // + // This must return at least as many bytes as the write() function will + // write. It can return more bytes than needed, for example when writing + // Strings we can't know the exact bytes needed until we the UTF-8 + // encoding, so we pessimistically allocate the largest size possible (3 + // bytes per codepoint). Allocating extra bytes is not really a big deal + // because the `RustBuffer` is short-lived. + fun allocationSize(value: KotlinType): ULong + + // Write a Kotlin type to a `ByteBuffer` + fun write(value: KotlinType, buf: ByteBuffer) + + // Lower a value into a `RustBuffer` + // + // This method lowers a value into a `RustBuffer` rather than the normal + // FfiType. It's used by the callback interface code. Callback interface + // returns are always serialized into a `RustBuffer` regardless of their + // normal FFI type. + fun lowerIntoRustBuffer(value: KotlinType): RustBuffer.ByValue { + val rbuf = RustBuffer.alloc(allocationSize(value)) + try { + val bbuf = rbuf.data!!.getByteBuffer(0, rbuf.capacity).also { + it.order(ByteOrder.BIG_ENDIAN) + } + write(value, bbuf) + rbuf.writeField("len", bbuf.position().toLong()) + return rbuf + } catch (e: Throwable) { + RustBuffer.free(rbuf) + throw e + } + } + + // Lift a value from a `RustBuffer`. + // + // This here mostly because of the symmetry with `lowerIntoRustBuffer()`. + // It's currently only used by the `FfiConverterRustBuffer` class below. + fun liftFromRustBuffer(rbuf: RustBuffer.ByValue): KotlinType { + val byteBuf = rbuf.asByteBuffer()!! + try { + val item = read(byteBuf) + if (byteBuf.hasRemaining()) { + throw RuntimeException("junk remaining in buffer after lifting, something is very wrong!!") + } + return item + } finally { + RustBuffer.free(rbuf) + } + } +} + +/** + * FfiConverter that uses `RustBuffer` as the FfiType + * + * @suppress + */ +public interface FfiConverterRustBuffer: FfiConverter { + override fun lift(value: RustBuffer.ByValue) = liftFromRustBuffer(value) + override fun lower(value: KotlinType) = lowerIntoRustBuffer(value) +} +// A handful of classes and functions to support the generated data structures. +// This would be a good candidate for isolating in its own ffi-support lib. + +internal const val UNIFFI_CALL_SUCCESS = 0.toByte() +internal const val UNIFFI_CALL_ERROR = 1.toByte() +internal const val UNIFFI_CALL_UNEXPECTED_ERROR = 2.toByte() + +@Structure.FieldOrder("code", "error_buf") +internal open class UniffiRustCallStatus : Structure() { + @JvmField var code: Byte = 0 + @JvmField var error_buf: RustBuffer.ByValue = RustBuffer.ByValue() + + class ByValue: UniffiRustCallStatus(), Structure.ByValue + + fun isSuccess(): Boolean { + return code == UNIFFI_CALL_SUCCESS + } + + fun isError(): Boolean { + return code == UNIFFI_CALL_ERROR + } + + fun isPanic(): Boolean { + return code == UNIFFI_CALL_UNEXPECTED_ERROR + } + + companion object { + fun create(code: Byte, errorBuf: RustBuffer.ByValue): UniffiRustCallStatus.ByValue { + val callStatus = UniffiRustCallStatus.ByValue() + callStatus.code = code + callStatus.error_buf = errorBuf + return callStatus + } + } +} + +class InternalException(message: String) : kotlin.Exception(message) + +/** + * Each top-level error class has a companion object that can lift the error from the call status's rust buffer + * + * @suppress + */ +interface UniffiRustCallStatusErrorHandler { + fun lift(error_buf: RustBuffer.ByValue): E; +} + +// Helpers for calling Rust +// In practice we usually need to be synchronized to call this safely, so it doesn't +// synchronize itself + +// Call a rust function that returns a Result<>. Pass in the Error class companion that corresponds to the Err +private inline fun uniffiRustCallWithError(errorHandler: UniffiRustCallStatusErrorHandler, callback: (UniffiRustCallStatus) -> U): U { + var status = UniffiRustCallStatus() + val return_value = callback(status) + uniffiCheckCallStatus(errorHandler, status) + return return_value +} + +// Check UniffiRustCallStatus and throw an error if the call wasn't successful +private fun uniffiCheckCallStatus(errorHandler: UniffiRustCallStatusErrorHandler, status: UniffiRustCallStatus) { + if (status.isSuccess()) { + return + } else if (status.isError()) { + throw errorHandler.lift(status.error_buf) + } else if (status.isPanic()) { + // when the rust code sees a panic, it tries to construct a rustbuffer + // with the message. but if that code panics, then it just sends back + // an empty buffer. + if (status.error_buf.len > 0) { + throw InternalException(FfiConverterString.lift(status.error_buf)) + } else { + throw InternalException("Rust panic") + } + } else { + throw InternalException("Unknown rust call status: $status.code") + } +} + +/** + * UniffiRustCallStatusErrorHandler implementation for times when we don't expect a CALL_ERROR + * + * @suppress + */ +object UniffiNullRustCallStatusErrorHandler: UniffiRustCallStatusErrorHandler { + override fun lift(error_buf: RustBuffer.ByValue): InternalException { + RustBuffer.free(error_buf) + return InternalException("Unexpected CALL_ERROR") + } +} + +// Call a rust function that returns a plain value +private inline fun uniffiRustCall(callback: (UniffiRustCallStatus) -> U): U { + return uniffiRustCallWithError(UniffiNullRustCallStatusErrorHandler, callback) +} + +internal inline fun uniffiTraitInterfaceCall( + callStatus: UniffiRustCallStatus, + makeCall: () -> T, + writeReturn: (T) -> Unit, +) { + try { + writeReturn(makeCall()) + } catch(e: kotlin.Exception) { + callStatus.code = UNIFFI_CALL_UNEXPECTED_ERROR + callStatus.error_buf = FfiConverterString.lower(e.toString()) + } +} + +internal inline fun uniffiTraitInterfaceCallWithError( + callStatus: UniffiRustCallStatus, + makeCall: () -> T, + writeReturn: (T) -> Unit, + lowerError: (E) -> RustBuffer.ByValue +) { + try { + writeReturn(makeCall()) + } catch(e: kotlin.Exception) { + if (e is E) { + callStatus.code = UNIFFI_CALL_ERROR + callStatus.error_buf = lowerError(e) + } else { + callStatus.code = UNIFFI_CALL_UNEXPECTED_ERROR + callStatus.error_buf = FfiConverterString.lower(e.toString()) + } + } +} +// Map handles to objects +// +// This is used pass an opaque 64-bit handle representing a foreign object to the Rust code. +internal class UniffiHandleMap { + private val map = ConcurrentHashMap() + private val counter = java.util.concurrent.atomic.AtomicLong(0) + + val size: Int + get() = map.size + + // Insert a new object into the handle map and get a handle for it + fun insert(obj: T): Long { + val handle = counter.getAndAdd(1) + map.put(handle, obj) + return handle + } + + // Get an object from the handle map + fun get(handle: Long): T { + return map.get(handle) ?: throw InternalException("UniffiHandleMap.get: Invalid handle") + } + + // Remove an entry from the handlemap and get the Kotlin object back + fun remove(handle: Long): T { + return map.remove(handle) ?: throw InternalException("UniffiHandleMap: Invalid handle") + } +} + +// Contains loading, initialization code, +// and the FFI Function declarations in a com.sun.jna.Library. +@Synchronized +private fun findLibraryName(componentName: String): String { + val libOverride = System.getProperty("uniffi.component.$componentName.libraryOverride") + if (libOverride != null) { + return libOverride + } + return "truapi_server" +} + +private inline fun loadIndirect( + componentName: String +): Lib { + return Native.load(findLibraryName(componentName), Lib::class.java) +} + +// Define FFI callback types +internal interface UniffiRustFutureContinuationCallback : com.sun.jna.Callback { + fun callback(`data`: Long,`pollResult`: Byte,) +} +internal interface UniffiForeignFutureFree : com.sun.jna.Callback { + fun callback(`handle`: Long,) +} +internal interface UniffiCallbackInterfaceFree : com.sun.jna.Callback { + fun callback(`handle`: Long,) +} +@Structure.FieldOrder("handle", "free") +internal open class UniffiForeignFuture( + @JvmField internal var `handle`: Long = 0.toLong(), + @JvmField internal var `free`: UniffiForeignFutureFree? = null, +) : Structure() { + class UniffiByValue( + `handle`: Long = 0.toLong(), + `free`: UniffiForeignFutureFree? = null, + ): UniffiForeignFuture(`handle`,`free`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFuture) { + `handle` = other.`handle` + `free` = other.`free` + } + +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructU8( + @JvmField internal var `returnValue`: Byte = 0.toByte(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Byte = 0.toByte(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructU8(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructU8) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteU8 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructU8.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructI8( + @JvmField internal var `returnValue`: Byte = 0.toByte(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Byte = 0.toByte(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructI8(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructI8) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteI8 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructI8.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructU16( + @JvmField internal var `returnValue`: Short = 0.toShort(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Short = 0.toShort(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructU16(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructU16) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteU16 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructU16.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructI16( + @JvmField internal var `returnValue`: Short = 0.toShort(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Short = 0.toShort(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructI16(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructI16) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteI16 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructI16.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructU32( + @JvmField internal var `returnValue`: Int = 0, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Int = 0, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructU32(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructU32) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteU32 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructU32.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructI32( + @JvmField internal var `returnValue`: Int = 0, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Int = 0, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructI32(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructI32) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteI32 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructI32.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructU64( + @JvmField internal var `returnValue`: Long = 0.toLong(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Long = 0.toLong(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructU64(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructU64) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteU64 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructU64.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructI64( + @JvmField internal var `returnValue`: Long = 0.toLong(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Long = 0.toLong(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructI64(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructI64) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteI64 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructI64.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructF32( + @JvmField internal var `returnValue`: Float = 0.0f, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Float = 0.0f, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructF32(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructF32) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteF32 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructF32.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructF64( + @JvmField internal var `returnValue`: Double = 0.0, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Double = 0.0, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructF64(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructF64) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteF64 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructF64.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructPointer( + @JvmField internal var `returnValue`: Pointer = Pointer.NULL, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Pointer = Pointer.NULL, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructPointer(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructPointer) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompletePointer : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructPointer.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructRustBuffer( + @JvmField internal var `returnValue`: RustBuffer.ByValue = RustBuffer.ByValue(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: RustBuffer.ByValue = RustBuffer.ByValue(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructRustBuffer(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructRustBuffer) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteRustBuffer : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructRustBuffer.UniffiByValue,) +} +@Structure.FieldOrder("callStatus") +internal open class UniffiForeignFutureStructVoid( + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructVoid(`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructVoid) { + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteVoid : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructVoid.UniffiByValue,) +} +internal interface UniffiCallbackInterfaceHostCallbacksMethod0 : com.sun.jna.Callback { + fun callback(`uniffiHandle`: Long,`marker`: RustBuffer.ByValue,`detail`: RustBuffer.ByValue,`uniffiOutReturn`: Pointer,uniffiCallStatus: UniffiRustCallStatus,) +} +internal interface UniffiCallbackInterfaceHostCallbacksMethod1 : com.sun.jna.Callback { + fun callback(`uniffiHandle`: Long,`frame`: RustBuffer.ByValue,`uniffiOutReturn`: Pointer,uniffiCallStatus: UniffiRustCallStatus,) +} +internal interface UniffiCallbackInterfaceHostCallbacksMethod2 : com.sun.jna.Callback { + fun callback(`uniffiHandle`: Long,`url`: RustBuffer.ByValue,`uniffiOutReturn`: Pointer,uniffiCallStatus: UniffiRustCallStatus,) +} +internal interface UniffiCallbackInterfaceHostCallbacksMethod3 : com.sun.jna.Callback { + fun callback(`uniffiHandle`: Long,`payload`: RustBuffer.ByValue,`uniffiOutReturn`: Pointer,uniffiCallStatus: UniffiRustCallStatus,) +} +internal interface UniffiCallbackInterfaceHostCallbacksMethod4 : com.sun.jna.Callback { + fun callback(`uniffiHandle`: Long,`request`: RustBuffer.ByValue,`uniffiOutReturn`: ByteByReference,uniffiCallStatus: UniffiRustCallStatus,) +} +internal interface UniffiCallbackInterfaceHostCallbacksMethod5 : com.sun.jna.Callback { + fun callback(`uniffiHandle`: Long,`request`: RustBuffer.ByValue,`uniffiOutReturn`: ByteByReference,uniffiCallStatus: UniffiRustCallStatus,) +} +internal interface UniffiCallbackInterfaceHostCallbacksMethod6 : com.sun.jna.Callback { + fun callback(`uniffiHandle`: Long,`request`: RustBuffer.ByValue,`uniffiOutReturn`: ByteByReference,uniffiCallStatus: UniffiRustCallStatus,) +} +internal interface UniffiCallbackInterfaceHostCallbacksMethod7 : com.sun.jna.Callback { + fun callback(`uniffiHandle`: Long,`key`: RustBuffer.ByValue,`uniffiOutReturn`: RustBuffer,uniffiCallStatus: UniffiRustCallStatus,) +} +internal interface UniffiCallbackInterfaceHostCallbacksMethod8 : com.sun.jna.Callback { + fun callback(`uniffiHandle`: Long,`key`: RustBuffer.ByValue,`value`: RustBuffer.ByValue,`uniffiOutReturn`: Pointer,uniffiCallStatus: UniffiRustCallStatus,) +} +internal interface UniffiCallbackInterfaceHostCallbacksMethod9 : com.sun.jna.Callback { + fun callback(`uniffiHandle`: Long,`key`: RustBuffer.ByValue,`uniffiOutReturn`: Pointer,uniffiCallStatus: UniffiRustCallStatus,) +} +@Structure.FieldOrder("onCoreLog", "onCoreResponse", "navigateTo", "pushNotification", "devicePermission", "remotePermission", "featureSupported", "localStorageRead", "localStorageWrite", "localStorageClear", "uniffiFree") +internal open class UniffiVTableCallbackInterfaceHostCallbacks( + @JvmField internal var `onCoreLog`: UniffiCallbackInterfaceHostCallbacksMethod0? = null, + @JvmField internal var `onCoreResponse`: UniffiCallbackInterfaceHostCallbacksMethod1? = null, + @JvmField internal var `navigateTo`: UniffiCallbackInterfaceHostCallbacksMethod2? = null, + @JvmField internal var `pushNotification`: UniffiCallbackInterfaceHostCallbacksMethod3? = null, + @JvmField internal var `devicePermission`: UniffiCallbackInterfaceHostCallbacksMethod4? = null, + @JvmField internal var `remotePermission`: UniffiCallbackInterfaceHostCallbacksMethod5? = null, + @JvmField internal var `featureSupported`: UniffiCallbackInterfaceHostCallbacksMethod6? = null, + @JvmField internal var `localStorageRead`: UniffiCallbackInterfaceHostCallbacksMethod7? = null, + @JvmField internal var `localStorageWrite`: UniffiCallbackInterfaceHostCallbacksMethod8? = null, + @JvmField internal var `localStorageClear`: UniffiCallbackInterfaceHostCallbacksMethod9? = null, + @JvmField internal var `uniffiFree`: UniffiCallbackInterfaceFree? = null, +) : Structure() { + class UniffiByValue( + `onCoreLog`: UniffiCallbackInterfaceHostCallbacksMethod0? = null, + `onCoreResponse`: UniffiCallbackInterfaceHostCallbacksMethod1? = null, + `navigateTo`: UniffiCallbackInterfaceHostCallbacksMethod2? = null, + `pushNotification`: UniffiCallbackInterfaceHostCallbacksMethod3? = null, + `devicePermission`: UniffiCallbackInterfaceHostCallbacksMethod4? = null, + `remotePermission`: UniffiCallbackInterfaceHostCallbacksMethod5? = null, + `featureSupported`: UniffiCallbackInterfaceHostCallbacksMethod6? = null, + `localStorageRead`: UniffiCallbackInterfaceHostCallbacksMethod7? = null, + `localStorageWrite`: UniffiCallbackInterfaceHostCallbacksMethod8? = null, + `localStorageClear`: UniffiCallbackInterfaceHostCallbacksMethod9? = null, + `uniffiFree`: UniffiCallbackInterfaceFree? = null, + ): UniffiVTableCallbackInterfaceHostCallbacks(`onCoreLog`,`onCoreResponse`,`navigateTo`,`pushNotification`,`devicePermission`,`remotePermission`,`featureSupported`,`localStorageRead`,`localStorageWrite`,`localStorageClear`,`uniffiFree`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiVTableCallbackInterfaceHostCallbacks) { + `onCoreLog` = other.`onCoreLog` + `onCoreResponse` = other.`onCoreResponse` + `navigateTo` = other.`navigateTo` + `pushNotification` = other.`pushNotification` + `devicePermission` = other.`devicePermission` + `remotePermission` = other.`remotePermission` + `featureSupported` = other.`featureSupported` + `localStorageRead` = other.`localStorageRead` + `localStorageWrite` = other.`localStorageWrite` + `localStorageClear` = other.`localStorageClear` + `uniffiFree` = other.`uniffiFree` + } + +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +// For large crates we prevent `MethodTooLargeException` (see #2340) +// N.B. the name of the extension is very misleading, since it is +// rather `InterfaceTooLargeException`, caused by too many methods +// in the interface for large crates. +// +// By splitting the otherwise huge interface into two parts +// * UniffiLib +// * IntegrityCheckingUniffiLib (this) +// we allow for ~2x as many methods in the UniffiLib interface. +// +// The `ffi_uniffi_contract_version` method and all checksum methods are put +// into `IntegrityCheckingUniffiLib` and these methods are called only once, +// when the library is loaded. +internal interface IntegrityCheckingUniffiLib : Library { + // Integrity check functions only + fun uniffi_truapi_server_checksum_method_nativetruapicore_clear_active_session( +): Short +fun uniffi_truapi_server_checksum_method_nativetruapicore_debug_smoke_feature_request_frame( +): Short +fun uniffi_truapi_server_checksum_method_nativetruapicore_receive_from_product( +): Short +fun uniffi_truapi_server_checksum_method_nativetruapicore_set_active_session( +): Short +fun uniffi_truapi_server_checksum_method_nativetruapicore_start_ws_bridge( +): Short +fun uniffi_truapi_server_checksum_method_nativetruapicore_stop_ws_bridge( +): Short +fun uniffi_truapi_server_checksum_constructor_nativetruapicore_new( +): Short +fun uniffi_truapi_server_checksum_method_hostcallbacks_on_core_log( +): Short +fun uniffi_truapi_server_checksum_method_hostcallbacks_on_core_response( +): Short +fun uniffi_truapi_server_checksum_method_hostcallbacks_navigate_to( +): Short +fun uniffi_truapi_server_checksum_method_hostcallbacks_push_notification( +): Short +fun uniffi_truapi_server_checksum_method_hostcallbacks_device_permission( +): Short +fun uniffi_truapi_server_checksum_method_hostcallbacks_remote_permission( +): Short +fun uniffi_truapi_server_checksum_method_hostcallbacks_feature_supported( +): Short +fun uniffi_truapi_server_checksum_method_hostcallbacks_local_storage_read( +): Short +fun uniffi_truapi_server_checksum_method_hostcallbacks_local_storage_write( +): Short +fun uniffi_truapi_server_checksum_method_hostcallbacks_local_storage_clear( +): Short +fun ffi_truapi_server_uniffi_contract_version( +): Int + +} + +// A JNA Library to expose the extern-C FFI definitions. +// This is an implementation detail which will be called internally by the public API. +internal interface UniffiLib : Library { + companion object { + internal val INSTANCE: UniffiLib by lazy { + val componentName = "truapi_server" + // For large crates we prevent `MethodTooLargeException` (see #2340) + // N.B. the name of the extension is very misleading, since it is + // rather `InterfaceTooLargeException`, caused by too many methods + // in the interface for large crates. + // + // By splitting the otherwise huge interface into two parts + // * UniffiLib (this) + // * IntegrityCheckingUniffiLib + // And all checksum methods are put into `IntegrityCheckingUniffiLib` + // we allow for ~2x as many methods in the UniffiLib interface. + // + // Thus we first load the library with `loadIndirect` as `IntegrityCheckingUniffiLib` + // so that we can (optionally!) call `uniffiCheckApiChecksums`... + loadIndirect(componentName) + .also { lib: IntegrityCheckingUniffiLib -> + uniffiCheckContractApiVersion(lib) + uniffiCheckApiChecksums(lib) + } + // ... and then we load the library as `UniffiLib` + // N.B. we cannot use `loadIndirect` once and then try to cast it to `UniffiLib` + // => results in `java.lang.ClassCastException: com.sun.proxy.$Proxy cannot be cast to ...` + // error. So we must call `loadIndirect` twice. For crates large enough + // to trigger this issue, the performance impact is negligible, running on + // a macOS M1 machine the `loadIndirect` call takes ~50ms. + val lib = loadIndirect(componentName) + // No need to check the contract version and checksums, since + // we already did that with `IntegrityCheckingUniffiLib` above. + uniffiCallbackInterfaceHostCallbacks.register(lib) + // Loading of library with integrity check done. + lib + } + + // The Cleaner for the whole library + internal val CLEANER: UniffiCleaner by lazy { + UniffiCleaner.create() + } + } + + // FFI functions + fun uniffi_truapi_server_fn_clone_nativetruapicore(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, +): Pointer +fun uniffi_truapi_server_fn_free_nativetruapicore(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, +): Unit +fun uniffi_truapi_server_fn_constructor_nativetruapicore_new(`callbacks`: Long,uniffi_out_err: UniffiRustCallStatus, +): Pointer +fun uniffi_truapi_server_fn_method_nativetruapicore_clear_active_session(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, +): Unit +fun uniffi_truapi_server_fn_method_nativetruapicore_debug_smoke_feature_request_frame(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, +): RustBuffer.ByValue +fun uniffi_truapi_server_fn_method_nativetruapicore_receive_from_product(`ptr`: Pointer,`frame`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, +): Byte +fun uniffi_truapi_server_fn_method_nativetruapicore_set_active_session(`ptr`: Pointer,`pubkey`: RustBuffer.ByValue,`liteUsername`: RustBuffer.ByValue,`fullUsername`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, +): Byte +fun uniffi_truapi_server_fn_method_nativetruapicore_start_ws_bridge(`ptr`: Pointer,`bindPort`: Short,uniffi_out_err: UniffiRustCallStatus, +): RustBuffer.ByValue +fun uniffi_truapi_server_fn_method_nativetruapicore_stop_ws_bridge(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, +): Unit +fun uniffi_truapi_server_fn_init_callback_vtable_hostcallbacks(`vtable`: UniffiVTableCallbackInterfaceHostCallbacks, +): Unit +fun ffi_truapi_server_rustbuffer_alloc(`size`: Long,uniffi_out_err: UniffiRustCallStatus, +): RustBuffer.ByValue +fun ffi_truapi_server_rustbuffer_from_bytes(`bytes`: ForeignBytes.ByValue,uniffi_out_err: UniffiRustCallStatus, +): RustBuffer.ByValue +fun ffi_truapi_server_rustbuffer_free(`buf`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, +): Unit +fun ffi_truapi_server_rustbuffer_reserve(`buf`: RustBuffer.ByValue,`additional`: Long,uniffi_out_err: UniffiRustCallStatus, +): RustBuffer.ByValue +fun ffi_truapi_server_rust_future_poll_u8(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, +): Unit +fun ffi_truapi_server_rust_future_cancel_u8(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_free_u8(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_complete_u8(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, +): Byte +fun ffi_truapi_server_rust_future_poll_i8(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, +): Unit +fun ffi_truapi_server_rust_future_cancel_i8(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_free_i8(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_complete_i8(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, +): Byte +fun ffi_truapi_server_rust_future_poll_u16(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, +): Unit +fun ffi_truapi_server_rust_future_cancel_u16(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_free_u16(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_complete_u16(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, +): Short +fun ffi_truapi_server_rust_future_poll_i16(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, +): Unit +fun ffi_truapi_server_rust_future_cancel_i16(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_free_i16(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_complete_i16(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, +): Short +fun ffi_truapi_server_rust_future_poll_u32(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, +): Unit +fun ffi_truapi_server_rust_future_cancel_u32(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_free_u32(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_complete_u32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, +): Int +fun ffi_truapi_server_rust_future_poll_i32(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, +): Unit +fun ffi_truapi_server_rust_future_cancel_i32(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_free_i32(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_complete_i32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, +): Int +fun ffi_truapi_server_rust_future_poll_u64(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, +): Unit +fun ffi_truapi_server_rust_future_cancel_u64(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_free_u64(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_complete_u64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, +): Long +fun ffi_truapi_server_rust_future_poll_i64(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, +): Unit +fun ffi_truapi_server_rust_future_cancel_i64(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_free_i64(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_complete_i64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, +): Long +fun ffi_truapi_server_rust_future_poll_f32(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, +): Unit +fun ffi_truapi_server_rust_future_cancel_f32(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_free_f32(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_complete_f32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, +): Float +fun ffi_truapi_server_rust_future_poll_f64(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, +): Unit +fun ffi_truapi_server_rust_future_cancel_f64(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_free_f64(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_complete_f64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, +): Double +fun ffi_truapi_server_rust_future_poll_pointer(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, +): Unit +fun ffi_truapi_server_rust_future_cancel_pointer(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_free_pointer(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_complete_pointer(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, +): Pointer +fun ffi_truapi_server_rust_future_poll_rust_buffer(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, +): Unit +fun ffi_truapi_server_rust_future_cancel_rust_buffer(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_free_rust_buffer(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_complete_rust_buffer(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, +): RustBuffer.ByValue +fun ffi_truapi_server_rust_future_poll_void(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, +): Unit +fun ffi_truapi_server_rust_future_cancel_void(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_free_void(`handle`: Long, +): Unit +fun ffi_truapi_server_rust_future_complete_void(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, +): Unit + +} + +private fun uniffiCheckContractApiVersion(lib: IntegrityCheckingUniffiLib) { + // Get the bindings contract version from our ComponentInterface + val bindings_contract_version = 29 + // Get the scaffolding contract version by calling the into the dylib + val scaffolding_contract_version = lib.ffi_truapi_server_uniffi_contract_version() + if (bindings_contract_version != scaffolding_contract_version) { + throw RuntimeException("UniFFI contract version mismatch: try cleaning and rebuilding your project") + } +} +@Suppress("UNUSED_PARAMETER") +private fun uniffiCheckApiChecksums(lib: IntegrityCheckingUniffiLib) { + if (lib.uniffi_truapi_server_checksum_method_nativetruapicore_clear_active_session() != 49688.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_truapi_server_checksum_method_nativetruapicore_debug_smoke_feature_request_frame() != 14429.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_truapi_server_checksum_method_nativetruapicore_receive_from_product() != 8699.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_truapi_server_checksum_method_nativetruapicore_set_active_session() != 33211.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_truapi_server_checksum_method_nativetruapicore_start_ws_bridge() != 64697.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_truapi_server_checksum_method_nativetruapicore_stop_ws_bridge() != 16007.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_truapi_server_checksum_constructor_nativetruapicore_new() != 3488.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_truapi_server_checksum_method_hostcallbacks_on_core_log() != 50767.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_truapi_server_checksum_method_hostcallbacks_on_core_response() != 8464.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_truapi_server_checksum_method_hostcallbacks_navigate_to() != 45479.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_truapi_server_checksum_method_hostcallbacks_push_notification() != 3163.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_truapi_server_checksum_method_hostcallbacks_device_permission() != 3805.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_truapi_server_checksum_method_hostcallbacks_remote_permission() != 10542.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_truapi_server_checksum_method_hostcallbacks_feature_supported() != 16480.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_truapi_server_checksum_method_hostcallbacks_local_storage_read() != 9342.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_truapi_server_checksum_method_hostcallbacks_local_storage_write() != 34181.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_truapi_server_checksum_method_hostcallbacks_local_storage_clear() != 58615.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } +} + +/** + * @suppress + */ +public fun uniffiEnsureInitialized() { + UniffiLib.INSTANCE +} + +// Async support + +// Public interface members begin here. + + +// Interface implemented by anything that can contain an object reference. +// +// Such types expose a `destroy()` method that must be called to cleanly +// dispose of the contained objects. Failure to call this method may result +// in memory leaks. +// +// The easiest way to ensure this method is called is to use the `.use` +// helper method to execute a block and destroy the object at the end. +interface Disposable { + fun destroy() + companion object { + fun destroy(vararg args: Any?) { + for (arg in args) { + when (arg) { + is Disposable -> arg.destroy() + is ArrayList<*> -> { + for (idx in arg.indices) { + val element = arg[idx] + if (element is Disposable) { + element.destroy() + } + } + } + is Map<*, *> -> { + for (element in arg.values) { + if (element is Disposable) { + element.destroy() + } + } + } + is Iterable<*> -> { + for (element in arg) { + if (element is Disposable) { + element.destroy() + } + } + } + } + } + } + } +} + +/** + * @suppress + */ +inline fun T.use(block: (T) -> R) = + try { + block(this) + } finally { + try { + // N.B. our implementation is on the nullable type `Disposable?`. + this?.destroy() + } catch (e: Throwable) { + // swallow + } + } + +/** + * Used to instantiate an interface without an actual pointer, for fakes in tests, mostly. + * + * @suppress + * */ +object NoPointer// Magic number for the Rust proxy to call using the same mechanism as every other method, +// to free the callback once it's dropped by Rust. +internal const val IDX_CALLBACK_FREE = 0 +// Callback return codes +internal const val UNIFFI_CALLBACK_SUCCESS = 0 +internal const val UNIFFI_CALLBACK_ERROR = 1 +internal const val UNIFFI_CALLBACK_UNEXPECTED_ERROR = 2 + +/** + * @suppress + */ +public abstract class FfiConverterCallbackInterface: FfiConverter { + internal val handleMap = UniffiHandleMap() + + internal fun drop(handle: Long) { + handleMap.remove(handle) + } + + override fun lift(value: Long): CallbackInterface { + return handleMap.get(value) + } + + override fun read(buf: ByteBuffer) = lift(buf.getLong()) + + override fun lower(value: CallbackInterface) = handleMap.insert(value) + + override fun allocationSize(value: CallbackInterface) = 8UL + + override fun write(value: CallbackInterface, buf: ByteBuffer) { + buf.putLong(lower(value)) + } +} +/** + * The cleaner interface for Object finalization code to run. + * This is the entry point to any implementation that we're using. + * + * The cleaner registers objects and returns cleanables, so now we are + * defining a `UniffiCleaner` with a `UniffiClenaer.Cleanable` to abstract the + * different implmentations available at compile time. + * + * @suppress + */ +interface UniffiCleaner { + interface Cleanable { + fun clean() + } + + fun register(value: Any, cleanUpTask: Runnable): UniffiCleaner.Cleanable + + companion object +} + +// The fallback Jna cleaner, which is available for both Android, and the JVM. +private class UniffiJnaCleaner : UniffiCleaner { + private val cleaner = com.sun.jna.internal.Cleaner.getCleaner() + + override fun register(value: Any, cleanUpTask: Runnable): UniffiCleaner.Cleanable = + UniffiJnaCleanable(cleaner.register(value, cleanUpTask)) +} + +private class UniffiJnaCleanable( + private val cleanable: com.sun.jna.internal.Cleaner.Cleanable, +) : UniffiCleaner.Cleanable { + override fun clean() = cleanable.clean() +} + + +// We decide at uniffi binding generation time whether we were +// using Android or not. +// There are further runtime checks to chose the correct implementation +// of the cleaner. +private fun UniffiCleaner.Companion.create(): UniffiCleaner = + try { + // For safety's sake: if the library hasn't been run in android_cleaner = true + // mode, but is being run on Android, then we still need to think about + // Android API versions. + // So we check if java.lang.ref.Cleaner is there, and use that… + java.lang.Class.forName("java.lang.ref.Cleaner") + JavaLangRefCleaner() + } catch (e: ClassNotFoundException) { + // … otherwise, fallback to the JNA cleaner. + UniffiJnaCleaner() + } + +private class JavaLangRefCleaner : UniffiCleaner { + val cleaner = java.lang.ref.Cleaner.create() + + override fun register(value: Any, cleanUpTask: Runnable): UniffiCleaner.Cleanable = + JavaLangRefCleanable(cleaner.register(value, cleanUpTask)) +} + +private class JavaLangRefCleanable( + val cleanable: java.lang.ref.Cleaner.Cleanable +) : UniffiCleaner.Cleanable { + override fun clean() = cleanable.clean() +} + +/** + * @suppress + */ +public object FfiConverterUShort: FfiConverter { + override fun lift(value: Short): UShort { + return value.toUShort() + } + + override fun read(buf: ByteBuffer): UShort { + return lift(buf.getShort()) + } + + override fun lower(value: UShort): Short { + return value.toShort() + } + + override fun allocationSize(value: UShort) = 2UL + + override fun write(value: UShort, buf: ByteBuffer) { + buf.putShort(value.toShort()) + } +} + +/** + * @suppress + */ +public object FfiConverterBoolean: FfiConverter { + override fun lift(value: Byte): Boolean { + return value.toInt() != 0 + } + + override fun read(buf: ByteBuffer): Boolean { + return lift(buf.get()) + } + + override fun lower(value: Boolean): Byte { + return if (value) 1.toByte() else 0.toByte() + } + + override fun allocationSize(value: Boolean) = 1UL + + override fun write(value: Boolean, buf: ByteBuffer) { + buf.put(lower(value)) + } +} + +/** + * @suppress + */ +public object FfiConverterString: FfiConverter { + // Note: we don't inherit from FfiConverterRustBuffer, because we use a + // special encoding when lowering/lifting. We can use `RustBuffer.len` to + // store our length and avoid writing it out to the buffer. + override fun lift(value: RustBuffer.ByValue): String { + try { + val byteArr = ByteArray(value.len.toInt()) + value.asByteBuffer()!!.get(byteArr) + return byteArr.toString(Charsets.UTF_8) + } finally { + RustBuffer.free(value) + } + } + + override fun read(buf: ByteBuffer): String { + val len = buf.getInt() + val byteArr = ByteArray(len) + buf.get(byteArr) + return byteArr.toString(Charsets.UTF_8) + } + + fun toUtf8(value: String): ByteBuffer { + // Make sure we don't have invalid UTF-16, check for lone surrogates. + return Charsets.UTF_8.newEncoder().run { + onMalformedInput(CodingErrorAction.REPORT) + encode(CharBuffer.wrap(value)) + } + } + + override fun lower(value: String): RustBuffer.ByValue { + val byteBuf = toUtf8(value) + // Ideally we'd pass these bytes to `ffi_bytebuffer_from_bytes`, but doing so would require us + // to copy them into a JNA `Memory`. So we might as well directly copy them into a `RustBuffer`. + val rbuf = RustBuffer.alloc(byteBuf.limit().toULong()) + rbuf.asByteBuffer()!!.put(byteBuf) + return rbuf + } + + // We aren't sure exactly how many bytes our string will be once it's UTF-8 + // encoded. Allocate 3 bytes per UTF-16 code unit which will always be + // enough. + override fun allocationSize(value: String): ULong { + val sizeForLength = 4UL + val sizeForString = value.length.toULong() * 3UL + return sizeForLength + sizeForString + } + + override fun write(value: String, buf: ByteBuffer) { + val byteBuf = toUtf8(value) + buf.putInt(byteBuf.limit()) + buf.put(byteBuf) + } +} + +/** + * @suppress + */ +public object FfiConverterByteArray: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): ByteArray { + val len = buf.getInt() + val byteArr = ByteArray(len) + buf.get(byteArr) + return byteArr + } + override fun allocationSize(value: ByteArray): ULong { + return 4UL + value.size.toULong() + } + override fun write(value: ByteArray, buf: ByteBuffer) { + buf.putInt(value.size) + buf.put(value) + } +} + + +// This template implements a class for working with a Rust struct via a Pointer/Arc +// to the live Rust struct on the other side of the FFI. +// +// Each instance implements core operations for working with the Rust `Arc` and the +// Kotlin Pointer to work with the live Rust struct on the other side of the FFI. +// +// There's some subtlety here, because we have to be careful not to operate on a Rust +// struct after it has been dropped, and because we must expose a public API for freeing +// theq Kotlin wrapper object in lieu of reliable finalizers. The core requirements are: +// +// * Each instance holds an opaque pointer to the underlying Rust struct. +// Method calls need to read this pointer from the object's state and pass it in to +// the Rust FFI. +// +// * When an instance is no longer needed, its pointer should be passed to a +// special destructor function provided by the Rust FFI, which will drop the +// underlying Rust struct. +// +// * Given an instance, calling code is expected to call the special +// `destroy` method in order to free it after use, either by calling it explicitly +// or by using a higher-level helper like the `use` method. Failing to do so risks +// leaking the underlying Rust struct. +// +// * We can't assume that calling code will do the right thing, and must be prepared +// to handle Kotlin method calls executing concurrently with or even after a call to +// `destroy`, and to handle multiple (possibly concurrent!) calls to `destroy`. +// +// * We must never allow Rust code to operate on the underlying Rust struct after +// the destructor has been called, and must never call the destructor more than once. +// Doing so may trigger memory unsafety. +// +// * To mitigate many of the risks of leaking memory and use-after-free unsafety, a `Cleaner` +// is implemented to call the destructor when the Kotlin object becomes unreachable. +// This is done in a background thread. This is not a panacea, and client code should be aware that +// 1. the thread may starve if some there are objects that have poorly performing +// `drop` methods or do significant work in their `drop` methods. +// 2. the thread is shared across the whole library. This can be tuned by using `android_cleaner = true`, +// or `android = true` in the [`kotlin` section of the `uniffi.toml` file](https://mozilla.github.io/uniffi-rs/kotlin/configuration.html). +// +// If we try to implement this with mutual exclusion on access to the pointer, there is the +// possibility of a race between a method call and a concurrent call to `destroy`: +// +// * Thread A starts a method call, reads the value of the pointer, but is interrupted +// before it can pass the pointer over the FFI to Rust. +// * Thread B calls `destroy` and frees the underlying Rust struct. +// * Thread A resumes, passing the already-read pointer value to Rust and triggering +// a use-after-free. +// +// One possible solution would be to use a `ReadWriteLock`, with each method call taking +// a read lock (and thus allowed to run concurrently) and the special `destroy` method +// taking a write lock (and thus blocking on live method calls). However, we aim not to +// generate methods with any hidden blocking semantics, and a `destroy` method that might +// block if called incorrectly seems to meet that bar. +// +// So, we achieve our goals by giving each instance an associated `AtomicLong` counter to track +// the number of in-flight method calls, and an `AtomicBoolean` flag to indicate whether `destroy` +// has been called. These are updated according to the following rules: +// +// * The initial value of the counter is 1, indicating a live object with no in-flight calls. +// The initial value for the flag is false. +// +// * At the start of each method call, we atomically check the counter. +// If it is 0 then the underlying Rust struct has already been destroyed and the call is aborted. +// If it is nonzero them we atomically increment it by 1 and proceed with the method call. +// +// * At the end of each method call, we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// * When `destroy` is called, we atomically flip the flag from false to true. +// If the flag was already true we silently fail. +// Otherwise we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// Astute readers may observe that this all sounds very similar to the way that Rust's `Arc` works, +// and indeed it is, with the addition of a flag to guard against multiple calls to `destroy`. +// +// The overall effect is that the underlying Rust struct is destroyed only when `destroy` has been +// called *and* all in-flight method calls have completed, avoiding violating any of the expectations +// of the underlying Rust code. +// +// This makes a cleaner a better alternative to _not_ calling `destroy()` as +// and when the object is finished with, but the abstraction is not perfect: if the Rust object's `drop` +// method is slow, and/or there are many objects to cleanup, and it's on a low end Android device, then the cleaner +// thread may be starved, and the app will leak memory. +// +// In this case, `destroy`ing manually may be a better solution. +// +// The cleaner can live side by side with the manual calling of `destroy`. In the order of responsiveness, uniffi objects +// with Rust peers are reclaimed: +// +// 1. By calling the `destroy` method of the object, which calls `rustObject.free()`. If that doesn't happen: +// 2. When the object becomes unreachable, AND the Cleaner thread gets to call `rustObject.free()`. If the thread is starved then: +// 3. The memory is reclaimed when the process terminates. +// +// [1] https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope/24380219 +// + + +/** + * UniFFI object exposing the TrUAPI core to native hosts. + */ +public interface NativeTrUApiCoreInterface { + + /** + * Drop the currently-paired session. Mirrors the JS + * `clearActiveSession`. + */ + fun `clearActiveSession`() + + /** + * Smoke-test helper: return a SCALE-encoded `feature_supported` + * request frame so the iOS/Android shells can verify the wire path + * without owning request construction logic. + */ + fun `debugSmokeFeatureRequestFrame`(): kotlin.ByteArray + + /** + * Push an inbound SCALE-encoded protocol frame from the product into + * the dispatcher. Responses are emitted back through the + * [`HostCallbacks::on_core_response`] callback. + */ + fun `receiveFromProduct`(`frame`: kotlin.ByteArray): kotlin.Boolean + + /** + * Push the currently-paired session into the core. Mirrors the JS + * `setActiveSession`. `pubkey` must be exactly 32 bytes (sr25519 root + * public key). + */ + fun `setActiveSession`(`pubkey`: kotlin.ByteArray, `liteUsername`: kotlin.String?, `fullUsername`: kotlin.String?): kotlin.Boolean + + /** + * Start the localhost WebSocket bridge. Returns the descriptor the + * host hands to the product so it can dial back in. + */ + fun `startWsBridge`(`bindPort`: kotlin.UShort): WsBridgeEndpoint + + /** + * Stop the localhost WebSocket bridge (if running). + */ + fun `stopWsBridge`() + + companion object +} + +/** + * UniFFI object exposing the TrUAPI core to native hosts. + */ +open class NativeTrUApiCore: Disposable, AutoCloseable, NativeTrUApiCoreInterface +{ + + constructor(pointer: Pointer) { + this.pointer = pointer + this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer)) + } + + /** + * This constructor can be used to instantiate a fake object. Only used for tests. Any + * attempt to actually use an object constructed this way will fail as there is no + * connected Rust object. + */ + @Suppress("UNUSED_PARAMETER") + constructor(noPointer: NoPointer) { + this.pointer = null + this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer)) + } + /** + * Construct the core from a callback object. The native shell hands + * over its [`HostCallbacks`] trait object; the core wraps it in a + * [`CallbackPlatform`] and feeds the result into + * [`TrUApiCore::from_platform`]. + * + * Subscriptions registered through this core run on a shared + * `futures::executor::ThreadPool`. The pool sticks around for the + * lifetime of the core; new subscriptions never spawn a fresh OS + * thread each. + */ + constructor(`callbacks`: HostCallbacks) : + this( + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_truapi_server_fn_constructor_nativetruapicore_new( + FfiConverterTypeHostCallbacks.lower(`callbacks`),_status) +} + ) + + protected val pointer: Pointer? + protected val cleanable: UniffiCleaner.Cleanable + + private val wasDestroyed = AtomicBoolean(false) + private val callCounter = AtomicLong(1) + + override fun destroy() { + // Only allow a single call to this method. + // TODO: maybe we should log a warning if called more than once? + if (this.wasDestroyed.compareAndSet(false, true)) { + // This decrement always matches the initial count of 1 given at creation time. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + @Synchronized + override fun close() { + this.destroy() + } + + internal inline fun callWithPointer(block: (ptr: Pointer) -> R): R { + // Check and increment the call counter, to keep the object alive. + // This needs a compare-and-set retry loop in case of concurrent updates. + do { + val c = this.callCounter.get() + if (c == 0L) { + throw IllegalStateException("${this.javaClass.simpleName} object has already been destroyed") + } + if (c == Long.MAX_VALUE) { + throw IllegalStateException("${this.javaClass.simpleName} call counter would overflow") + } + } while (! this.callCounter.compareAndSet(c, c + 1L)) + // Now we can safely do the method call without the pointer being freed concurrently. + try { + return block(this.uniffiClonePointer()) + } finally { + // This decrement always matches the increment we performed above. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + // Use a static inner class instead of a closure so as not to accidentally + // capture `this` as part of the cleanable's action. + private class UniffiCleanAction(private val pointer: Pointer?) : Runnable { + override fun run() { + pointer?.let { ptr -> + uniffiRustCall { status -> + UniffiLib.INSTANCE.uniffi_truapi_server_fn_free_nativetruapicore(ptr, status) + } + } + } + } + + fun uniffiClonePointer(): Pointer { + return uniffiRustCall() { status -> + UniffiLib.INSTANCE.uniffi_truapi_server_fn_clone_nativetruapicore(pointer!!, status) + } + } + + + /** + * Drop the currently-paired session. Mirrors the JS + * `clearActiveSession`. + */override fun `clearActiveSession`() + = + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_truapi_server_fn_method_nativetruapicore_clear_active_session( + it, _status) +} + } + + + + + /** + * Smoke-test helper: return a SCALE-encoded `feature_supported` + * request frame so the iOS/Android shells can verify the wire path + * without owning request construction logic. + */override fun `debugSmokeFeatureRequestFrame`(): kotlin.ByteArray { + return FfiConverterByteArray.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_truapi_server_fn_method_nativetruapicore_debug_smoke_feature_request_frame( + it, _status) +} + } + ) + } + + + + /** + * Push an inbound SCALE-encoded protocol frame from the product into + * the dispatcher. Responses are emitted back through the + * [`HostCallbacks::on_core_response`] callback. + */override fun `receiveFromProduct`(`frame`: kotlin.ByteArray): kotlin.Boolean { + return FfiConverterBoolean.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_truapi_server_fn_method_nativetruapicore_receive_from_product( + it, FfiConverterByteArray.lower(`frame`),_status) +} + } + ) + } + + + + /** + * Push the currently-paired session into the core. Mirrors the JS + * `setActiveSession`. `pubkey` must be exactly 32 bytes (sr25519 root + * public key). + */override fun `setActiveSession`(`pubkey`: kotlin.ByteArray, `liteUsername`: kotlin.String?, `fullUsername`: kotlin.String?): kotlin.Boolean { + return FfiConverterBoolean.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_truapi_server_fn_method_nativetruapicore_set_active_session( + it, FfiConverterByteArray.lower(`pubkey`),FfiConverterOptionalString.lower(`liteUsername`),FfiConverterOptionalString.lower(`fullUsername`),_status) +} + } + ) + } + + + + /** + * Start the localhost WebSocket bridge. Returns the descriptor the + * host hands to the product so it can dial back in. + */ + @Throws(WsBridgeStartException::class)override fun `startWsBridge`(`bindPort`: kotlin.UShort): WsBridgeEndpoint { + return FfiConverterTypeWsBridgeEndpoint.lift( + callWithPointer { + uniffiRustCallWithError(WsBridgeStartException) { _status -> + UniffiLib.INSTANCE.uniffi_truapi_server_fn_method_nativetruapicore_start_ws_bridge( + it, FfiConverterUShort.lower(`bindPort`),_status) +} + } + ) + } + + + + /** + * Stop the localhost WebSocket bridge (if running). + */override fun `stopWsBridge`() + = + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_truapi_server_fn_method_nativetruapicore_stop_ws_bridge( + it, _status) +} + } + + + + + + + + companion object + +} + +/** + * @suppress + */ +public object FfiConverterTypeNativeTrUApiCore: FfiConverter { + + override fun lower(value: NativeTrUApiCore): Pointer { + return value.uniffiClonePointer() + } + + override fun lift(value: Pointer): NativeTrUApiCore { + return NativeTrUApiCore(value) + } + + override fun read(buf: ByteBuffer): NativeTrUApiCore { + // The Rust code always writes pointers as 8 bytes, and will + // fail to compile if they don't fit. + return lift(Pointer(buf.getLong())) + } + + override fun allocationSize(value: NativeTrUApiCore) = 8UL + + override fun write(value: NativeTrUApiCore, buf: ByteBuffer) { + // The Rust code always expects pointers written as 8 bytes, + // and will fail to compile if they don't fit. + buf.putLong(Pointer.nativeValue(lower(value))) + } +} + + + +/** + * Per-session descriptor returned to the host: product uses `port + token` + * to build its WebSocket URL (e.g. `ws://127.0.0.1:/?t=`). + */ +data class WsBridgeEndpoint ( + /** + * Localhost port the bridge is listening on. + */ + var `port`: kotlin.UShort, + /** + * Session token; the connecting client must supply this as the + * `?t=` query parameter to be accepted. + */ + var `token`: kotlin.String +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeWsBridgeEndpoint: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): WsBridgeEndpoint { + return WsBridgeEndpoint( + FfiConverterUShort.read(buf), + FfiConverterString.read(buf), + ) + } + + override fun allocationSize(value: WsBridgeEndpoint) = ( + FfiConverterUShort.allocationSize(value.`port`) + + FfiConverterString.allocationSize(value.`token`) + ) + + override fun write(value: WsBridgeEndpoint, buf: ByteBuffer) { + FfiConverterUShort.write(value.`port`, buf) + FfiConverterString.write(value.`token`, buf) + } +} + + + + + +/** + * Native-friendly navigation error. + */ +sealed class HostNavigateRejection: kotlin.Exception() { + + /** + * User declined the navigation. + */ + class PermissionDenied( + ) : HostNavigateRejection() { + override val message + get() = "" + } + + /** + * Catch-all. + */ + class Unknown( + + /** + * Human-readable reason. + */ + val `reason`: kotlin.String + ) : HostNavigateRejection() { + override val message + get() = "reason=${ `reason` }" + } + + + companion object ErrorHandler : UniffiRustCallStatusErrorHandler { + override fun lift(error_buf: RustBuffer.ByValue): HostNavigateRejection = FfiConverterTypeHostNavigateRejection.lift(error_buf) + } + + +} + +/** + * @suppress + */ +public object FfiConverterTypeHostNavigateRejection : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): HostNavigateRejection { + + + return when(buf.getInt()) { + 1 -> HostNavigateRejection.PermissionDenied() + 2 -> HostNavigateRejection.Unknown( + FfiConverterString.read(buf), + ) + else -> throw RuntimeException("invalid error enum value, something is very wrong!!") + } + } + + override fun allocationSize(value: HostNavigateRejection): ULong { + return when(value) { + is HostNavigateRejection.PermissionDenied -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + ) + is HostNavigateRejection.Unknown -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.`reason`) + ) + } + } + + override fun write(value: HostNavigateRejection, buf: ByteBuffer) { + when(value) { + is HostNavigateRejection.PermissionDenied -> { + buf.putInt(1) + Unit + } + is HostNavigateRejection.Unknown -> { + buf.putInt(2) + FfiConverterString.write(value.`reason`, buf) + Unit + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } + +} + + + + + +/** + * Native-friendly rejection error returned by callback methods that map + * onto [`truapi::v01::GenericError`]. + */ +sealed class HostRejection: kotlin.Exception() { + + /** + * Caller rejected the operation. + */ + class Rejected( + + /** + * Human-readable rejection reason. + */ + val `reason`: kotlin.String + ) : HostRejection() { + override val message + get() = "reason=${ `reason` }" + } + + + companion object ErrorHandler : UniffiRustCallStatusErrorHandler { + override fun lift(error_buf: RustBuffer.ByValue): HostRejection = FfiConverterTypeHostRejection.lift(error_buf) + } + + +} + +/** + * @suppress + */ +public object FfiConverterTypeHostRejection : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): HostRejection { + + + return when(buf.getInt()) { + 1 -> HostRejection.Rejected( + FfiConverterString.read(buf), + ) + else -> throw RuntimeException("invalid error enum value, something is very wrong!!") + } + } + + override fun allocationSize(value: HostRejection): ULong { + return when(value) { + is HostRejection.Rejected -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.`reason`) + ) + } + } + + override fun write(value: HostRejection, buf: ByteBuffer) { + when(value) { + is HostRejection.Rejected -> { + buf.putInt(1) + FfiConverterString.write(value.`reason`, buf) + Unit + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } + +} + + + + + +/** + * Native-friendly storage error. Mirrors the v0.1 wire shape so the + * callback surface stays SCALE-free. + */ +sealed class HostStorageException: kotlin.Exception() { + + /** + * Quota exhausted. + */ + class Full( + ) : HostStorageException() { + override val message + get() = "" + } + + /** + * Catch-all. + */ + class Unknown( + + /** + * Human-readable failure reason. + */ + val `reason`: kotlin.String + ) : HostStorageException() { + override val message + get() = "reason=${ `reason` }" + } + + + companion object ErrorHandler : UniffiRustCallStatusErrorHandler { + override fun lift(error_buf: RustBuffer.ByValue): HostStorageException = FfiConverterTypeHostStorageError.lift(error_buf) + } + + +} + +/** + * @suppress + */ +public object FfiConverterTypeHostStorageError : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): HostStorageException { + + + return when(buf.getInt()) { + 1 -> HostStorageException.Full() + 2 -> HostStorageException.Unknown( + FfiConverterString.read(buf), + ) + else -> throw RuntimeException("invalid error enum value, something is very wrong!!") + } + } + + override fun allocationSize(value: HostStorageException): ULong { + return when(value) { + is HostStorageException.Full -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + ) + is HostStorageException.Unknown -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.`reason`) + ) + } + } + + override fun write(value: HostStorageException, buf: ByteBuffer) { + when(value) { + is HostStorageException.Full -> { + buf.putInt(1) + Unit + } + is HostStorageException.Unknown -> { + buf.putInt(2) + FfiConverterString.write(value.`reason`, buf) + Unit + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } + +} + + + + + +/** + * Failure modes returned from host-facing `start_ws_bridge` wrappers. + */ +sealed class WsBridgeStartException(message: String): kotlin.Exception(message) { + + /** + * A bridge is already running for this host. + */ + class AlreadyRunning(message: String) : WsBridgeStartException(message) + + /** + * Anything else (bind failure, runtime spin-up failure, ...). + */ + class Io(message: String) : WsBridgeStartException(message) + + + companion object ErrorHandler : UniffiRustCallStatusErrorHandler { + override fun lift(error_buf: RustBuffer.ByValue): WsBridgeStartException = FfiConverterTypeWsBridgeStartError.lift(error_buf) + } +} + +/** + * @suppress + */ +public object FfiConverterTypeWsBridgeStartError : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): WsBridgeStartException { + + return when(buf.getInt()) { + 1 -> WsBridgeStartException.AlreadyRunning(FfiConverterString.read(buf)) + 2 -> WsBridgeStartException.Io(FfiConverterString.read(buf)) + else -> throw RuntimeException("invalid error enum value, something is very wrong!!") + } + + } + + override fun allocationSize(value: WsBridgeStartException): ULong { + return 4UL + } + + override fun write(value: WsBridgeStartException, buf: ByteBuffer) { + when(value) { + is WsBridgeStartException.AlreadyRunning -> { + buf.putInt(1) + Unit + } + is WsBridgeStartException.Io -> { + buf.putInt(2) + Unit + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } + +} + + + + + +/** + * Callback surface that iOS and Android implement. The Rust core invokes + * these synchronously from `async` trait methods, which is acceptable for + * UniFFI because every callback hop is short-lived and reentrant. + */ +public interface HostCallbacks { + + /** + * Lifecycle logger. Marker is a stable slug, detail is free-form. + */ + fun `onCoreLog`(`marker`: kotlin.String, `detail`: kotlin.String) + + /** + * Forward an outbound protocol frame (already SCALE-encoded) to the + * product. The native shell pumps these into the in-app messaging + * channel. + */ + fun `onCoreResponse`(`frame`: kotlin.ByteArray) + + /** + * Open a URL in the system browser. + */ + fun `navigateTo`(`url`: kotlin.String) + + /** + * Deliver a push notification. The payload is the SCALE-encoded + * [`v01::HostPushNotificationRequest`]. + */ + fun `pushNotification`(`payload`: kotlin.ByteArray) + + /** + * Prompt the user for a device-level permission (camera, mic, ...). + * `request` is the SCALE-encoded + * [`v01::HostDevicePermissionRequest`]; the host returns whether the + * permission was granted. + */ + fun `devicePermission`(`request`: kotlin.ByteArray): kotlin.Boolean + + /** + * Prompt the user for a remote (product-scoped) permission bundle. + * `request` is the SCALE-encoded [`v01::RemotePermissionRequest`]. + */ + fun `remotePermission`(`request`: kotlin.ByteArray): kotlin.Boolean + + /** + * Answer a feature-support query. `request` is the SCALE-encoded + * [`HostFeatureSupportedRequest`]. + */ + fun `featureSupported`(`request`: kotlin.ByteArray): kotlin.Boolean + + /** + * Read a value from the host's scoped key-value store. + */ + fun `localStorageRead`(`key`: kotlin.String): kotlin.ByteArray? + + /** + * Write a value to the host's scoped key-value store. + */ + fun `localStorageWrite`(`key`: kotlin.String, `value`: kotlin.ByteArray) + + /** + * Clear a value from the host's scoped key-value store. + */ + fun `localStorageClear`(`key`: kotlin.String) + + companion object +} + + + +// Put the implementation in an object so we don't pollute the top-level namespace +internal object uniffiCallbackInterfaceHostCallbacks { + internal object `onCoreLog`: UniffiCallbackInterfaceHostCallbacksMethod0 { + override fun callback(`uniffiHandle`: Long,`marker`: RustBuffer.ByValue,`detail`: RustBuffer.ByValue,`uniffiOutReturn`: Pointer,uniffiCallStatus: UniffiRustCallStatus,) { + val uniffiObj = FfiConverterTypeHostCallbacks.handleMap.get(uniffiHandle) + val makeCall = { -> + uniffiObj.`onCoreLog`( + FfiConverterString.lift(`marker`), + FfiConverterString.lift(`detail`), + ) + } + val writeReturn = { _: Unit -> Unit } + uniffiTraitInterfaceCall(uniffiCallStatus, makeCall, writeReturn) + } + } + internal object `onCoreResponse`: UniffiCallbackInterfaceHostCallbacksMethod1 { + override fun callback(`uniffiHandle`: Long,`frame`: RustBuffer.ByValue,`uniffiOutReturn`: Pointer,uniffiCallStatus: UniffiRustCallStatus,) { + val uniffiObj = FfiConverterTypeHostCallbacks.handleMap.get(uniffiHandle) + val makeCall = { -> + uniffiObj.`onCoreResponse`( + FfiConverterByteArray.lift(`frame`), + ) + } + val writeReturn = { _: Unit -> Unit } + uniffiTraitInterfaceCall(uniffiCallStatus, makeCall, writeReturn) + } + } + internal object `navigateTo`: UniffiCallbackInterfaceHostCallbacksMethod2 { + override fun callback(`uniffiHandle`: Long,`url`: RustBuffer.ByValue,`uniffiOutReturn`: Pointer,uniffiCallStatus: UniffiRustCallStatus,) { + val uniffiObj = FfiConverterTypeHostCallbacks.handleMap.get(uniffiHandle) + val makeCall = { -> + uniffiObj.`navigateTo`( + FfiConverterString.lift(`url`), + ) + } + val writeReturn = { _: Unit -> Unit } + uniffiTraitInterfaceCallWithError( + uniffiCallStatus, + makeCall, + writeReturn, + { e: HostNavigateRejection -> FfiConverterTypeHostNavigateRejection.lower(e) } + ) + } + } + internal object `pushNotification`: UniffiCallbackInterfaceHostCallbacksMethod3 { + override fun callback(`uniffiHandle`: Long,`payload`: RustBuffer.ByValue,`uniffiOutReturn`: Pointer,uniffiCallStatus: UniffiRustCallStatus,) { + val uniffiObj = FfiConverterTypeHostCallbacks.handleMap.get(uniffiHandle) + val makeCall = { -> + uniffiObj.`pushNotification`( + FfiConverterByteArray.lift(`payload`), + ) + } + val writeReturn = { _: Unit -> Unit } + uniffiTraitInterfaceCallWithError( + uniffiCallStatus, + makeCall, + writeReturn, + { e: HostRejection -> FfiConverterTypeHostRejection.lower(e) } + ) + } + } + internal object `devicePermission`: UniffiCallbackInterfaceHostCallbacksMethod4 { + override fun callback(`uniffiHandle`: Long,`request`: RustBuffer.ByValue,`uniffiOutReturn`: ByteByReference,uniffiCallStatus: UniffiRustCallStatus,) { + val uniffiObj = FfiConverterTypeHostCallbacks.handleMap.get(uniffiHandle) + val makeCall = { -> + uniffiObj.`devicePermission`( + FfiConverterByteArray.lift(`request`), + ) + } + val writeReturn = { value: kotlin.Boolean -> uniffiOutReturn.setValue(FfiConverterBoolean.lower(value)) } + uniffiTraitInterfaceCallWithError( + uniffiCallStatus, + makeCall, + writeReturn, + { e: HostRejection -> FfiConverterTypeHostRejection.lower(e) } + ) + } + } + internal object `remotePermission`: UniffiCallbackInterfaceHostCallbacksMethod5 { + override fun callback(`uniffiHandle`: Long,`request`: RustBuffer.ByValue,`uniffiOutReturn`: ByteByReference,uniffiCallStatus: UniffiRustCallStatus,) { + val uniffiObj = FfiConverterTypeHostCallbacks.handleMap.get(uniffiHandle) + val makeCall = { -> + uniffiObj.`remotePermission`( + FfiConverterByteArray.lift(`request`), + ) + } + val writeReturn = { value: kotlin.Boolean -> uniffiOutReturn.setValue(FfiConverterBoolean.lower(value)) } + uniffiTraitInterfaceCallWithError( + uniffiCallStatus, + makeCall, + writeReturn, + { e: HostRejection -> FfiConverterTypeHostRejection.lower(e) } + ) + } + } + internal object `featureSupported`: UniffiCallbackInterfaceHostCallbacksMethod6 { + override fun callback(`uniffiHandle`: Long,`request`: RustBuffer.ByValue,`uniffiOutReturn`: ByteByReference,uniffiCallStatus: UniffiRustCallStatus,) { + val uniffiObj = FfiConverterTypeHostCallbacks.handleMap.get(uniffiHandle) + val makeCall = { -> + uniffiObj.`featureSupported`( + FfiConverterByteArray.lift(`request`), + ) + } + val writeReturn = { value: kotlin.Boolean -> uniffiOutReturn.setValue(FfiConverterBoolean.lower(value)) } + uniffiTraitInterfaceCallWithError( + uniffiCallStatus, + makeCall, + writeReturn, + { e: HostRejection -> FfiConverterTypeHostRejection.lower(e) } + ) + } + } + internal object `localStorageRead`: UniffiCallbackInterfaceHostCallbacksMethod7 { + override fun callback(`uniffiHandle`: Long,`key`: RustBuffer.ByValue,`uniffiOutReturn`: RustBuffer,uniffiCallStatus: UniffiRustCallStatus,) { + val uniffiObj = FfiConverterTypeHostCallbacks.handleMap.get(uniffiHandle) + val makeCall = { -> + uniffiObj.`localStorageRead`( + FfiConverterString.lift(`key`), + ) + } + val writeReturn = { value: kotlin.ByteArray? -> uniffiOutReturn.setValue(FfiConverterOptionalByteArray.lower(value)) } + uniffiTraitInterfaceCallWithError( + uniffiCallStatus, + makeCall, + writeReturn, + { e: HostStorageException -> FfiConverterTypeHostStorageError.lower(e) } + ) + } + } + internal object `localStorageWrite`: UniffiCallbackInterfaceHostCallbacksMethod8 { + override fun callback(`uniffiHandle`: Long,`key`: RustBuffer.ByValue,`value`: RustBuffer.ByValue,`uniffiOutReturn`: Pointer,uniffiCallStatus: UniffiRustCallStatus,) { + val uniffiObj = FfiConverterTypeHostCallbacks.handleMap.get(uniffiHandle) + val makeCall = { -> + uniffiObj.`localStorageWrite`( + FfiConverterString.lift(`key`), + FfiConverterByteArray.lift(`value`), + ) + } + val writeReturn = { _: Unit -> Unit } + uniffiTraitInterfaceCallWithError( + uniffiCallStatus, + makeCall, + writeReturn, + { e: HostStorageException -> FfiConverterTypeHostStorageError.lower(e) } + ) + } + } + internal object `localStorageClear`: UniffiCallbackInterfaceHostCallbacksMethod9 { + override fun callback(`uniffiHandle`: Long,`key`: RustBuffer.ByValue,`uniffiOutReturn`: Pointer,uniffiCallStatus: UniffiRustCallStatus,) { + val uniffiObj = FfiConverterTypeHostCallbacks.handleMap.get(uniffiHandle) + val makeCall = { -> + uniffiObj.`localStorageClear`( + FfiConverterString.lift(`key`), + ) + } + val writeReturn = { _: Unit -> Unit } + uniffiTraitInterfaceCallWithError( + uniffiCallStatus, + makeCall, + writeReturn, + { e: HostStorageException -> FfiConverterTypeHostStorageError.lower(e) } + ) + } + } + + internal object uniffiFree: UniffiCallbackInterfaceFree { + override fun callback(handle: Long) { + FfiConverterTypeHostCallbacks.handleMap.remove(handle) + } + } + + internal var vtable = UniffiVTableCallbackInterfaceHostCallbacks.UniffiByValue( + `onCoreLog`, + `onCoreResponse`, + `navigateTo`, + `pushNotification`, + `devicePermission`, + `remotePermission`, + `featureSupported`, + `localStorageRead`, + `localStorageWrite`, + `localStorageClear`, + uniffiFree, + ) + + // Registers the foreign callback with the Rust side. + // This method is generated for each callback interface. + internal fun register(lib: UniffiLib) { + lib.uniffi_truapi_server_fn_init_callback_vtable_hostcallbacks(vtable) + } +} + +/** + * The ffiConverter which transforms the Callbacks in to handles to pass to Rust. + * + * @suppress + */ +public object FfiConverterTypeHostCallbacks: FfiConverterCallbackInterface() + + + + +/** + * @suppress + */ +public object FfiConverterOptionalString: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): kotlin.String? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterString.read(buf) + } + + override fun allocationSize(value: kotlin.String?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterString.allocationSize(value) + } + } + + override fun write(value: kotlin.String?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterString.write(value, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterOptionalByteArray: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): kotlin.ByteArray? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterByteArray.read(buf) + } + + override fun allocationSize(value: kotlin.ByteArray?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterByteArray.allocationSize(value) + } + } + + override fun write(value: kotlin.ByteArray?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterByteArray.write(value, buf) + } + } +} + diff --git a/host-libs/android/src/main/kotlin/io/parity/truapi/TrUAPIHost.kt b/host-libs/android/src/main/kotlin/io/parity/truapi/TrUAPIHost.kt new file mode 100644 index 00000000..449bf8dd --- /dev/null +++ b/host-libs/android/src/main/kotlin/io/parity/truapi/TrUAPIHost.kt @@ -0,0 +1,267 @@ +// TrUAPIHost - Android host adapter. +// +// The Rust core (compiled to `libtruapi_server.so` and surfaced via UniFFI in +// `src/main/kotlin/generated/uniffi/truapi_server/truapi_server.kt`) owns the +// wire protocol, request routing, subscription lifecycle, and platform trait +// dispatch. +// +// This file exposes two things on top of the generated bindings: +// +// * `HostBridge` - a Kotlin-friendly callback interface the embedding app +// implements. It splits device and remote permissions, mirroring the +// `Permissions` platform trait in the Rust core. +// * `WebViewTransport` - a thin byte transport that forwards opaque wire +// bytes between a `WebView` and the Rust core. Bytes traverse the +// `JavascriptInterface` boundary as base64 because the bridge cannot +// carry binary types directly. +// +// The transport is independent of the core: tests can stand up a +// `WebViewTransport` against a non-UniFFI stub by implementing `CoreInbound`. + +package io.parity.truapi + +import android.util.Base64 +import android.webkit.JavascriptInterface +import android.webkit.WebView +import uniffi.truapi_server.HostCallbacks +import uniffi.truapi_server.HostNavigateRejection +import uniffi.truapi_server.HostRejection +import uniffi.truapi_server.HostStorageException +import uniffi.truapi_server.NativeTrUApiCore +import uniffi.truapi_server.WsBridgeEndpoint +import uniffi.truapi_server.WsBridgeStartException + +/** Package metadata. */ +object TrUAPIHost { + const val VERSION = "0.1.0" +} + +/** + * Storage backend the host provides to the Rust core. Throws + * [HostStorageException] to signal quota exhaustion or unknown failure; the + * core maps both onto the v0.1 `HostLocalStorageReadError` wire shape. + */ +interface HostStorage { + @Throws(HostStorageException::class) + fun read(key: String): ByteArray? + + @Throws(HostStorageException::class) + fun write(key: String, value: ByteArray) + + @Throws(HostStorageException::class) + fun clear(key: String) +} + +/** + * Host-side callback bundle that the Rust core invokes for capabilities the + * native shell owns. The interface mirrors the underlying UniFFI surface + * but keeps the permission split explicit: + * + * * [devicePermission] handles camera / mic / push prompts and similar + * OS-scoped grants. `request` is a SCALE-encoded + * `v01::HostDevicePermissionRequest`. + * * [remotePermission] handles per-product capability bundles requested + * by the application running inside the WebView. `request` is a + * SCALE-encoded `v01::RemotePermissionRequest`. + * + * Embedders typically wire the SCALE payloads through the generated + * `@parity/truapi` client running on the JS side for UI rendering, then + * report the user's decision as a `Boolean`. + */ +interface HostBridge { + /** Lifecycle logger. Marker is a stable slug, detail is free-form. */ + fun onCoreLog(marker: String, detail: String) {} + + /** Forward an outbound SCALE-encoded protocol frame to the product. */ + fun onCoreResponse(frame: ByteArray) + + /** Open a URL in the system browser. */ + @Throws(HostNavigateRejection::class) + fun navigateTo(url: String) + + /** Deliver a push notification (SCALE-encoded `HostPushNotificationRequest`). */ + @Throws(HostRejection::class) + fun pushNotification(payload: ByteArray) + + /** Prompt for a device-level permission. Returns whether it was granted. */ + @Throws(HostRejection::class) + fun devicePermission(request: ByteArray): Boolean + + /** Prompt for a remote (product-scoped) permission bundle. */ + @Throws(HostRejection::class) + fun remotePermission(request: ByteArray): Boolean + + /** Answer a feature-support query. */ + @Throws(HostRejection::class) + fun featureSupported(request: ByteArray): Boolean + + /** Scoped key-value storage for the Rust core. */ + val storage: HostStorage +} + +/** + * Adapter from the public [HostBridge] surface to the generated UniFFI + * [HostCallbacks] interface. Keeps the public API stable even if uniffi-bindgen + * renames generated symbols. + */ +private class HostCallbackAdapter(private val bridge: HostBridge) : HostCallbacks { + override fun onCoreLog(marker: String, detail: String) = + bridge.onCoreLog(marker, detail) + + override fun onCoreResponse(frame: ByteArray) = + bridge.onCoreResponse(frame) + + override fun navigateTo(url: String) = + bridge.navigateTo(url) + + override fun pushNotification(payload: ByteArray) = + bridge.pushNotification(payload) + + override fun devicePermission(request: ByteArray): Boolean = + bridge.devicePermission(request) + + override fun remotePermission(request: ByteArray): Boolean = + bridge.remotePermission(request) + + override fun featureSupported(request: ByteArray): Boolean = + bridge.featureSupported(request) + + override fun localStorageRead(key: String): ByteArray? = + bridge.storage.read(key) + + override fun localStorageWrite(key: String, value: ByteArray) = + bridge.storage.write(key, value) + + override fun localStorageClear(key: String) = + bridge.storage.clear(key) +} + +/** + * Sink for opaque wire frames coming from the WebView. The Rust core is the + * typical implementor (via [TrUAPIHostCore]); tests may use a stub. + */ +fun interface CoreInbound { + fun receiveFromProduct(frame: ByteArray) +} + +/** + * Owning wrapper around the Rust-backed [NativeTrUApiCore]. Implements + * [CoreInbound] so a [WebViewTransport] can deliver inbound frames directly, + * and exposes session and WS bridge controls. + * + * The wrapper holds a strong reference to the bridge so the JNA callback + * registration stays alive for the lifetime of the core. + */ +class TrUAPIHostCore(bridge: HostBridge) : CoreInbound, AutoCloseable { + @Suppress("unused") // retained to keep JNA callbacks alive + private val callbackRetainer: HostCallbacks = HostCallbackAdapter(bridge) + private val inner: NativeTrUApiCore = NativeTrUApiCore(callbackRetainer) + + override fun receiveFromProduct(frame: ByteArray): Unit { + inner.receiveFromProduct(frame) + } + + /** Set the currently-paired session. `pubkey` must be exactly 32 bytes. */ + fun setActiveSession( + pubkey: ByteArray, + liteUsername: String? = null, + fullUsername: String? = null, + ): Boolean = inner.setActiveSession(pubkey, liteUsername, fullUsername) + + /** Drop the currently-paired session. */ + fun clearActiveSession() { + inner.clearActiveSession() + } + + /** Start the localhost WebSocket bridge (requires the `ws-bridge` feature in the cdylib). */ + @Throws(WsBridgeStartException::class) + fun startWsBridge(bindPort: UShort = 0u): WsBridgeEndpoint = + inner.startWsBridge(bindPort) + + /** Stop the localhost WebSocket bridge (if running). */ + fun stopWsBridge() { + inner.stopWsBridge() + } + + /** Smoke-test helper: returns a SCALE-encoded `feature_supported` request frame. */ + fun debugSmokeFeatureRequestFrame(): ByteArray = + inner.debugSmokeFeatureRequestFrame() + + override fun close() { + inner.close() + } +} + +/** + * Wraps a [WebView] and forwards opaque wire bytes between JS and [core]. + * Attach with [attach] before loading the page so the JS shim is installed. + */ +class WebViewTransport( + private val webView: WebView, + private val core: CoreInbound, + private val callbackName: String = "__trUApiReceive", + private val interfaceName: String = "TrUApi", +) { + fun attach() { + webView.addJavascriptInterface(JsInterface(), interfaceName) + } + + fun detach() { + webView.removeJavascriptInterface(interfaceName) + } + + /** + * JS bootstrap to inject at document start so the page exposes a + * `window.trUApi` byte-pipe matching the JS host adapter shape. + */ + val bootstrapScript: String = """ + (function() { + var listeners = []; + window.$callbackName = function(b64) { + try { + var bin = atob(b64); + var bytes = new Uint8Array(bin.length); + for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); + listeners.forEach(function(l) { l(bytes); }); + } catch (e) { console.error('trUApi recv error', e); } + }; + function toB64(u8) { + var s = ''; + for (var i = 0; i < u8.length; i++) s += String.fromCharCode(u8[i]); + return btoa(s); + } + window.trUApi = { + postMessage: function(bytes) { + window.$interfaceName.postMessage(toB64(bytes)); + }, + subscribe: function(cb) { + listeners.push(cb); + return function() { + var i = listeners.indexOf(cb); + if (i >= 0) listeners.splice(i, 1); + }; + } + }; + window.dispatchEvent(new Event('truapi-native-ready')); + })(); + """.trimIndent() + + /** Called when the core has bytes to push to the product app. */ + fun sendToProduct(frame: ByteArray) { + val b64 = Base64.encodeToString(frame, Base64.NO_WRAP) + val js = "window.$callbackName && window.$callbackName('$b64')" + webView.post { webView.evaluateJavascript(js, null) } + } + + private inner class JsInterface { + @JavascriptInterface + fun postMessage(b64: String) { + val frame = try { + Base64.decode(b64, Base64.NO_WRAP) + } catch (_: IllegalArgumentException) { + return + } + core.receiveFromProduct(frame) + } + } +} diff --git a/host-libs/ios/TrUAPIHost/.gitignore b/host-libs/ios/TrUAPIHost/.gitignore new file mode 100644 index 00000000..11cc1dd7 --- /dev/null +++ b/host-libs/ios/TrUAPIHost/.gitignore @@ -0,0 +1,4 @@ +.build/ +DerivedData/ +*.xcodeproj/xcuserdata/ +Package.resolved diff --git a/host-libs/ios/TrUAPIHost/Package.swift b/host-libs/ios/TrUAPIHost/Package.swift new file mode 100644 index 00000000..d96ddd88 --- /dev/null +++ b/host-libs/ios/TrUAPIHost/Package.swift @@ -0,0 +1,35 @@ +// swift-tools-version: 5.9 +// +// TrUAPI iOS host package. +// +// The `truapi_serverFFI` target wraps the UniFFI-generated C header + module +// map so the generated Swift bindings can `import truapi_serverFFI`. The +// `TrUAPIHost` target contains both the generated Swift bindings and the +// thin host shell defined in `TrUAPIHost.swift`. +// +// Consumers must link a prebuilt `libtruapi_server` static or dynamic +// library when integrating into their app target. This package does not +// vendor the binary itself; see README.md for build instructions. + +import PackageDescription + +let package = Package( + name: "TrUAPIHost", + platforms: [.iOS(.v16), .macOS(.v13)], + products: [ + .library(name: "TrUAPIHost", targets: ["TrUAPIHost"]), + ], + targets: [ + .systemLibrary( + name: "truapi_serverFFI", + path: "Sources/truapi_serverFFI", + pkgConfig: nil, + providers: [] + ), + .target( + name: "TrUAPIHost", + dependencies: ["truapi_serverFFI"], + path: "Sources/TrUAPIHost" + ), + ] +) diff --git a/host-libs/ios/TrUAPIHost/README.md b/host-libs/ios/TrUAPIHost/README.md new file mode 100644 index 00000000..2e4ddafe --- /dev/null +++ b/host-libs/ios/TrUAPIHost/README.md @@ -0,0 +1,125 @@ +# TrUAPI iOS host adapter + +*Thin Swift shell over the Rust TrUAPI core (UniFFI) plus a `WKWebView` byte transport. Wire decoding, request routing, and subscription lifecycle stay in the Rust core.* + +## What this package is for + +The public surface lives in [`Sources/TrUAPIHost/TrUAPIHost.swift`](Sources/TrUAPIHost/TrUAPIHost.swift): + +- `HostBridge` - callback bundle the embedding app implements. Split into device permissions, remote permissions, navigation, push, feature support, and scoped storage. +- `TrUAPIHostCore` - owning wrapper around the UniFFI-generated `NativeTrUApiCore`. Implements `CoreInbound`, owns the bridge lifetime, exposes session and WS bridge controls. +- `WebViewTransport` - base64-over-`WKScriptMessageHandler` byte pipe between a `WKWebView` and any `CoreInbound`. Installs a `window.trUApi` shim that matches the JS host adapter shape. +- `LocalhostBridgeBootstrap` - script for the localhost WebSocket bridge mode (when the cdylib is built with `--features ws-bridge` and `TrUAPIHostCore.startWsBridge(...)` is invoked). + +The generated UniFFI bindings live alongside the shell in `Sources/TrUAPIHost/truapi_server.swift` and the C header / module map in `Sources/truapi_serverFFI/include/`. They are committed (they're large and consumers should not need a Rust toolchain). + +## Architecture + +```text +product app in WKWebView + Uint8Array frames via window.trUApi + | + v +WebViewTransport + base64 over WKScriptMessageHandler + | + v +TrUAPIHostCore (CoreInbound) + → uniffi → libtruapi_server +``` + +For embedded apps that prefer the localhost WebSocket bridge: + +```text +product app in WKWebView + binary frames via localhost WebSocket + | + v +Rust core WS bridge (started via startWsBridge) + | + v +Rust core dispatcher +``` + +## Permissions split + +The core's `Permissions` platform trait has two methods, and so does the bridge: + +- `devicePermission(request:)` - OS-scoped grants (camera, mic, location, push). `request` is a SCALE-encoded `v01::HostDevicePermissionRequest`. +- `remotePermission(request:)` - per-product capability bundles. `request` is a SCALE-encoded `v01::RemotePermissionRequest`. + +Both return a `Bool` granted flag. SCALE decoding for the UI prompt is done by the `@parity/truapi` JS client (or any consumer that links the protocol crate's types directly). + +## Example + +```swift +import Foundation +import WebKit +import TrUAPIHost + +final class MyStorage: HostStorageBackend, @unchecked Sendable { + private var map: [String: Data] = [:] + func read(key: String) throws -> Data? { map[key] } + func write(key: String, value: Data) throws { map[key] = value } + func clear(key: String) throws { map.removeValue(forKey: key) } +} + +final class MyBridge: HostBridge, @unchecked Sendable { + let storage: HostStorageBackend = MyStorage() + weak var transport: WebViewTransport? + + func onCoreResponse(frame: Data) { + Task { @MainActor in transport?.sendToProduct(frame) } + } + + func navigateTo(url: String) throws { /* open in browser */ } + func pushNotification(payload: Data) throws { /* show notification */ } + func devicePermission(request: Data) throws -> Bool { false } + func remotePermission(request: Data) throws -> Bool { false } + func featureSupported(request: Data) throws -> Bool { false } +} + +let bridge = MyBridge() +let core = TrUAPIHostCore(bridge: bridge) + +let contentController = WKUserContentController() +let configuration = WKWebViewConfiguration() +configuration.userContentController = contentController +let webView = WKWebView(frame: .zero, configuration: configuration) + +let transport = WebViewTransport(webView: webView, core: core) +bridge.transport = transport +transport.attach(to: contentController) +``` + +## Linking the cdylib + +This package does not vendor `libtruapi_server` - integrators link a prebuilt static or dynamic library when building the app target. Typical workflow: + +```bash +cargo build -p truapi-server --release --features ws-bridge \ + --target aarch64-apple-ios +cargo build -p truapi-server --release --features ws-bridge \ + --target aarch64-apple-ios-sim +``` + +Then either bundle the `.a` files as a `.xcframework` and add it under "Frameworks, Libraries, and Embedded Content" in the app target, or link directly via `OTHER_LDFLAGS`. + +## Regenerating the bindings + +The committed bindings under `Sources/TrUAPIHost/truapi_server.swift` and `Sources/truapi_serverFFI/include/` are produced from the workspace `uniffi-bindgen-cli`. The CLI emits `truapi_server.swift`, `truapi_serverFFI.h`, and `truapi_serverFFI.modulemap` into a single output directory; the modulemap is renamed to `module.modulemap` and the header is colocated under `Sources/truapi_serverFFI/include/` so SwiftPM's `systemLibrary` target picks them up. + +```bash +cargo build -p truapi-server --release --features ws-bridge +mkdir -p /tmp/uniffi-swift-out +cargo run -p uniffi-bindgen-cli -- generate \ + --library target/release/libtruapi_server.so \ + --language swift \ + --out-dir /tmp/uniffi-swift-out +cp /tmp/uniffi-swift-out/truapi_server.swift \ + host-libs/ios/TrUAPIHost/Sources/TrUAPIHost/truapi_server.swift +cp /tmp/uniffi-swift-out/truapi_serverFFI.h \ + host-libs/ios/TrUAPIHost/Sources/truapi_serverFFI/include/truapi_serverFFI.h +cp /tmp/uniffi-swift-out/truapi_serverFFI.modulemap \ + host-libs/ios/TrUAPIHost/Sources/truapi_serverFFI/include/module.modulemap +``` diff --git a/host-libs/ios/TrUAPIHost/Sources/TrUAPIHost/TrUAPIHost.swift b/host-libs/ios/TrUAPIHost/Sources/TrUAPIHost/TrUAPIHost.swift new file mode 100644 index 00000000..bd7fc1b4 --- /dev/null +++ b/host-libs/ios/TrUAPIHost/Sources/TrUAPIHost/TrUAPIHost.swift @@ -0,0 +1,306 @@ +// TrUAPIHost - iOS host adapter. +// +// The Rust core (compiled to `libtruapi_server`, surfaced through UniFFI in +// the sibling `truapi_server.swift` file) owns wire decoding, request +// routing, subscription lifecycle, and platform trait dispatch. +// +// This file layers two things on top of the generated bindings: +// +// * `HostBridge` - a Swift-friendly callback bundle the embedding app +// implements. It splits device and remote permissions, mirroring the +// `Permissions` platform trait in the Rust core. +// * `WebViewTransport` - a base64-over-`WKScriptMessageHandler` byte pipe +// between a `WKWebView` and any `CoreInbound`. +// +// `LocalhostBridgeBootstrap` is retained from earlier iOS shells for hosts +// that prefer the localhost WebSocket bridge over the direct WK script +// bridge. + +import Foundation +import WebKit + +/// Package metadata. +public enum TrUAPIHost { + public static let version = "0.1.0" +} + +/// Bootstrap helper for the native localhost WebSocket bridge that the Rust +/// core can stand up via `NativeTrUApiCore.startWsBridge(bindPort:)` when +/// the cdylib is built with the `ws-bridge` feature. +public enum LocalhostBridgeBootstrap { + /// Returns a `