diff --git a/.gitmodules b/.gitmodules index ffa7a3c9..dff6d927 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,7 @@ path = hosts/dotli url = https://github.com/paritytech/dotli-community branch = main +[submodule "hosts/ios"] + path = hosts/ios + url = https://github.com/paritytech/polkadot-app-ios-v2 + branch = codex/truapi-rust-core diff --git a/Cargo.lock b/Cargo.lock index f6aaa021..ed7e0c6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,6 +120,48 @@ 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-channel" version = "2.5.0" @@ -292,6 +334,15 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + [[package]] name = "bip39" version = "2.2.2" @@ -413,6 +464,38 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +[[package]] +name = "camino" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "cast" version = "0.3.0" @@ -709,6 +792,12 @@ dependencies = [ "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" @@ -908,7 +997,7 @@ dependencies = [ "foldhash 0.2.0", "libm", "portable-atomic", - "siphasher", + "siphasher 1.0.3", ] [[package]] @@ -999,6 +1088,15 @@ dependencies = [ "scale-info", ] +[[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" @@ -1172,6 +1270,12 @@ dependencies = [ "polyval", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "gloo-net" version = "0.6.0" @@ -1218,6 +1322,17 @@ dependencies = [ "web-sys", ] +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + [[package]] name = "group" version = "0.13.0" @@ -1839,6 +1954,12 @@ dependencies = [ "walkdir", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "mio" version = "1.2.1" @@ -1862,6 +1983,16 @@ 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" @@ -2088,6 +2219,12 @@ dependencies = [ "spki", ] +[[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" @@ -2459,6 +2596,26 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sec1" version = "0.7.3" @@ -2500,6 +2657,10 @@ 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" @@ -2645,6 +2806,12 @@ 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" @@ -2663,6 +2830,12 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smawk" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100" + [[package]] name = "smol" version = "2.0.2" @@ -2711,7 +2884,7 @@ dependencies = [ "libm", "libsecp256k1", "merlin", - "nom", + "nom 8.0.0", "num-bigint", "num-rational", "num-traits", @@ -2726,7 +2899,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "sha3", - "siphasher", + "siphasher 1.0.3", "slab", "smallvec", "soketto", @@ -2767,7 +2940,7 @@ dependencies = [ "libm", "libsecp256k1", "merlin", - "nom", + "nom 8.0.0", "num-bigint", "num-rational", "num-traits", @@ -2782,7 +2955,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "sha3", - "siphasher", + "siphasher 1.0.3", "slab", "smallvec", "soketto", @@ -2821,7 +2994,7 @@ dependencies = [ "rand_chacha", "serde", "serde_json", - "siphasher", + "siphasher 1.0.3", "slab", "smol", "smoldot 2.0.0", @@ -3000,6 +3173,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -3080,6 +3262,7 @@ version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ + "bytes", "libc", "mio", "pin-project-lite", @@ -3120,6 +3303,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -3134,6 +3329,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -3152,7 +3356,7 @@ dependencies = [ "indexmap", "toml_datetime", "toml_parser", - "winnow", + "winnow 1.0.2", ] [[package]] @@ -3161,7 +3365,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow", + "winnow 1.0.2", ] [[package]] @@ -3273,6 +3477,7 @@ dependencies = [ "parity-scale-codec", "pin-project", "primitive-types", + "rand", "schnorrkel", "send_wrapper 0.6.0", "serde", @@ -3281,11 +3486,14 @@ dependencies = [ "sp-crypto-hashing", "subxt-rpcs", "thiserror 1.0.69", + "tokio", + "tokio-tungstenite", "tracing", "tracing-subscriber", "truapi", "truapi-platform", "unicode-normalization", + "uniffi", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -3294,6 +3502,25 @@ dependencies = [ "web-time", ] +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror 1.0.69", + "url", + "utf-8", +] + [[package]] name = "twox-hash" version = "1.6.3" @@ -3356,6 +3583,134 @@ 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 0.3.11", + "uniffi_internal_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_pipeline" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd76b3ac8a2d964ca9fce7df21c755afb4c77b054a85ad7a029ad179cc5abb8a" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "tempfile", + "uniffi_internal_macros", +] + +[[package]] +name = "uniffi_udl" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319cf905911d70d5b97ce0f46f101619a22e9a189c8c46d797a9955e9233716" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "weedle2", +] + [[package]] name = "universal-hash" version = "0.5.1" @@ -3384,6 +3739,12 @@ dependencies = [ "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" @@ -3652,6 +4013,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weedle2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -3824,6 +4194,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[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" diff --git a/Makefile b/Makefile index 285aa467..efec4783 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ # Run `make help` for the list of targets. .DEFAULT_GOAL := help -.PHONY: help setup build codegen test check playground wasm wasm-crypto-test dev dev-bootstrap dev-link-check e2e-dotli matrix explorer +.PHONY: help setup build codegen test check playground wasm wasm-crypto-test uniffi dev dev-bootstrap dev-link-check e2e-dotli matrix explorer TRUAPI_PKG := js/packages/truapi PLAYGROUND := playground @@ -58,6 +58,32 @@ wasm: ## Rebuild the truapi-server WASM artifacts under js/packages/truapi-host- wasm-crypto-test: ## Run crypto/vector tests on wasm32 via wasm-pack/node. wasm-pack test --node rust/crates/truapi-server --test wasm_crypto_vectors --no-default-features +UNIFFI_CDYLIB_DIR := target/release +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Darwin) +UNIFFI_CDYLIB := $(UNIFFI_CDYLIB_DIR)/libtruapi_server.dylib +else +UNIFFI_CDYLIB := $(UNIFFI_CDYLIB_DIR)/libtruapi_server.so +endif + +UNIFFI_SWIFT_TMP := target/uniffi-swift-out + +uniffi: ## Regenerate Swift bindings from truapi-server cdylib. + cargo build -p truapi-server --release --features ws-bridge + rm -rf $(UNIFFI_SWIFT_TMP) + mkdir -p $(UNIFFI_SWIFT_TMP) + cargo run -p uniffi-bindgen-cli -- generate \ + --library $(UNIFFI_CDYLIB) \ + --language swift \ + --out-dir $(UNIFFI_SWIFT_TMP) + mkdir -p ios/truapi-host/Sources/truapi_serverFFI/include + cp $(UNIFFI_SWIFT_TMP)/truapi_server.swift \ + ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift + cp $(UNIFFI_SWIFT_TMP)/truapi_serverFFI.h \ + ios/truapi-host/Sources/truapi_serverFFI/include/truapi_serverFFI.h + cp $(UNIFFI_SWIFT_TMP)/truapi_serverFFI.modulemap \ + ios/truapi-host/Sources/truapi_serverFFI/include/module.modulemap + test: ## Run Rust + TypeScript client tests. cargo test --workspace cd $(TRUAPI_PKG) && npm test diff --git a/hosts/ios b/hosts/ios new file mode 160000 index 00000000..915f8d13 --- /dev/null +++ b/hosts/ios @@ -0,0 +1 @@ +Subproject commit 915f8d135ee5fde3fe7bf5a9192318cbc82ecbfb diff --git a/ios/truapi-host/.gitignore b/ios/truapi-host/.gitignore new file mode 100644 index 00000000..11cc1dd7 --- /dev/null +++ b/ios/truapi-host/.gitignore @@ -0,0 +1,4 @@ +.build/ +DerivedData/ +*.xcodeproj/xcuserdata/ +Package.resolved diff --git a/ios/truapi-host/Package.swift b/ios/truapi-host/Package.swift new file mode 100644 index 00000000..e7764729 --- /dev/null +++ b/ios/truapi-host/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/include", + pkgConfig: nil, + providers: [] + ), + .target( + name: "TrUAPIHost", + dependencies: ["truapi_serverFFI"], + path: "Sources/TrUAPIHost" + ), + ] +) diff --git a/ios/truapi-host/README.md b/ios/truapi-host/README.md new file mode 100644 index 00000000..d0975577 --- /dev/null +++ b/ios/truapi-host/README.md @@ -0,0 +1,204 @@ +# TrUAPI iOS host adapter + +*Thin Swift shell over the Rust TrUAPI core (UniFFI). Wire decoding, request routing, and subscription lifecycle stay in the Rust core; products connect through the localhost WebSocket bridge.* + +## 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. +- `HostStorageBackend` - product-scoped read/write/clear protocol the host backs with its own persistence. +- `HostCoreStorageBackend` - core-owned read/write/clear protocol for auth session, pairing identity, and persisted permission decisions. +- `TrUAPIHostCore` - owning wrapper around the UniFFI-generated `NativeTrUApiCore`. Holds the bridge alive for the lifetime of the core and exposes the localhost WebSocket bridge, core-owned disconnect, and native change notifications for core storage, theme, and preimage updates. +- `LocalhostBridgeBootstrap` - helper that produces a JS snippet publishing the WS bridge endpoint to the product page so it can dial back in. + +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 ignored build outputs; regenerate them before building or publishing the Swift package. + +## Architecture + +```text +product app in WKWebView + Uint8Array frames via @parity/truapi createWebSocketProvider + | + v ws://127.0.0.1:/?t= +TrUAPIHostCore.startWsBridge() + → libtruapi_server (tokio WS server) + → Rust dispatcher +``` + +The product running in the `WKWebView` opens a `WebSocket` to the localhost port + token returned by `startWsBridge`. From there the Rust core handles the wire protocol directly. Outbound responses and host-side capability callbacks (`navigateTo`, `pushNotification`, `cancelNotification`, `devicePermission`, `remotePermission`, `authStateChanged`, core storage, chain JSON-RPC, confirmations, preimage, theme, `featureSupported`, `storage`) reach the embedder through `HostBridge`. + +## 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 + +> **Threading:** the Rust core invokes every `HostBridge` callback on a +> background thread it owns, never the main thread. Hop to the main thread +> (`DispatchQueue.main` / `MainActor`) before touching UIKit, WebKit, or the +> `WKWebView`. UI-decision callbacks (`navigateTo`, `devicePermission`, +> `remotePermission`, `confirmUserAction`, `submitPreimage`) each run on +> their own blocking-pool thread, so it is safe to use +> `DispatchQueue.main.sync` (or a semaphore) to present the prompt on the +> main thread and block the calling thread until the user decides; other +> TrUAPI traffic keeps flowing while you wait. The remaining callbacks (auth +> state, storage, core storage, chain, feature, theme, preimage lookups) run +> inline on the dispatcher thread and must return promptly without blocking. + +```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 MyCoreStorage: HostCoreStorageBackend, @unchecked Sendable { + private var map: [Data: Data] = [:] + func read(key: Data) throws -> Data? { map[key] } + func write(key: Data, value: Data) throws { map[key] = value } + func clear(key: Data) throws { map.removeValue(forKey: key) } +} + +final class MyBridge: HostBridge, @unchecked Sendable { + let storage: HostStorageBackend = MyStorage() + let coreStorage: HostCoreStorageBackend = MyCoreStorage() + + // Callbacks arrive on background threads, never the main thread. + // Hop to the main thread before touching UIKit/WebKit. + func navigateTo(url: String) throws { + DispatchQueue.main.async { /* UIApplication.shared.open(...) */ } + } + + func pushNotification(payload: Data) throws -> UInt32 { + let id: UInt32 = 1 + DispatchQueue.main.async { /* schedule notification */ } + return id + } + + func cancelNotification(id: UInt32) throws { + DispatchQueue.main.async { /* cancel notification */ } + } + + func devicePermission(request: Data) throws -> Bool { + // Called on a blocking-pool thread; present synchronously on the main + // thread and return the decision. Blocking here does not stall other + // TrUAPI traffic. + DispatchQueue.main.sync { /* show prompt; */ false } + } + + func remotePermission(request: Data) throws -> Bool { + DispatchQueue.main.sync { /* show prompt; */ false } + } + + // Core-owned auth state stream: render `.pairing` as the pairing QR + // sheet, `.connected`/`.disconnected` as the account badge, and + // `.loginFailed` as a retryable error. When the user closes the pairing + // sheet, report it with `core.cancelLogin()`. + func authStateChanged(state: AuthState) { + DispatchQueue.main.async { /* render the state */ } + } + + func featureSupported(request: Data) throws -> Bool { false } + + func chainConnect(genesisHash: Data) throws -> UInt32? { + let id: UInt32 = 1 + DispatchQueue.main.async { /* open JSON-RPC connection, forward responses via core.notifyChainResponse */ } + return id + } + + func chainSend(connectionId: UInt32, request: String) throws { + /* send JSON-RPC request on the host connection */ + } + + func chainClose(connectionId: UInt32) throws { + /* close host connection */ + } + + func confirmUserAction(review: Data) throws -> Bool { + DispatchQueue.main.sync { /* render decoded UserConfirmationReview; */ false } + } +} + +let bridge = MyBridge() +let runtimeConfig = RuntimeConfig( + productId: "my-product.dot", + hostName: "My Host", + hostIcon: "https://host.example/icon.png", + peopleChainGenesisHash: Data(repeating: 0, count: 32), + pairingDeeplinkScheme: .polkadotApp +) +let core = try TrUAPIHostCore(bridge: bridge, runtimeConfig: runtimeConfig) +let endpoint = try core.startWsBridge() + +// Call these from host/platform observers so native subscriptions see updates +// after their immediate current item. +core.notifySessionStoreChanged() +core.notifyThemeChanged(theme: .dark) +core.notifyPreimageChanged(key: preimageKey, value: preimageBytesOrNil) +core.notifyChainResponse(connectionId: chainConnectionId, json: jsonRpcResponse) +core.notifyChainClosed(connectionId: chainConnectionId) + +let contentController = WKUserContentController() +let bootstrapScript = LocalhostBridgeBootstrap.script(port: endpoint.port, token: endpoint.token) +let userScript = WKUserScript( + source: bootstrapScript, + injectionTime: .atDocumentStart, + forMainFrameOnly: true +) +contentController.addUserScript(userScript) + +let configuration = WKWebViewConfiguration() +configuration.userContentController = contentController +let webView = WKWebView(frame: .zero, configuration: configuration) +webView.load(URLRequest(url: URL(string: "https://your-product.example/")!)) + +// On logout: +core.disconnect() +``` + +The product page reads `window.__truapi_localhost.url` (set by the bootstrap script) and passes it to `@parity/truapi`'s `createWebSocketProvider(url)`. + +## 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 ignored bindings under `Sources/TrUAPIHost/truapi_server.swift` and `Sources/truapi_serverFFI/include/` are produced from the workspace `uniffi-bindgen-cli`. Regenerate them before building or publishing the Swift package. 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 \ + ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift +cp /tmp/uniffi-swift-out/truapi_serverFFI.h \ + ios/truapi-host/Sources/truapi_serverFFI/include/truapi_serverFFI.h +cp /tmp/uniffi-swift-out/truapi_serverFFI.modulemap \ + ios/truapi-host/Sources/truapi_serverFFI/include/module.modulemap +``` + +Or run `make uniffi` from the repo root. diff --git a/ios/truapi-host/Sources/TrUAPIHost/TrUAPIHost.swift b/ios/truapi-host/Sources/TrUAPIHost/TrUAPIHost.swift new file mode 100644 index 00000000..de6c28c1 --- /dev/null +++ b/ios/truapi-host/Sources/TrUAPIHost/TrUAPIHost.swift @@ -0,0 +1,563 @@ +// 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 exposes: +// +// * `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. +// * `TrUAPIHostCore` - owning wrapper around the UniFFI-generated +// `NativeTrUApiCore`. Holds the bridge alive for the lifetime of the +// core and exposes session + WS-bridge controls. +// * `LocalhostBridgeBootstrap` - small JS snippet that publishes the WS +// bridge endpoint to the product page so it can dial back in. +// +// Products running inside a `WKWebView` connect to the Rust core via the +// localhost WebSocket bridge. The bootstrap script publishes the URL +// (`ws://127.0.0.1:/?t=`) and a MessagePort-shaped compatibility +// object that proxies the product's existing webview transport onto it. + +import Foundation + +/// Package metadata. +public enum TrUAPIHost { + public static let version = "0.1.0" +} + +/// Deeplink scheme used when the Rust core builds SSO pairing payloads. +public enum PairingDeeplinkScheme: Sendable { + case polkadotApp + case polkadotAppDev + + fileprivate var native: NativePairingDeeplinkScheme { + switch self { + case .polkadotApp: + return .polkadotApp + case .polkadotAppDev: + return .polkadotAppDev + } + } +} + +/// Static product and pairing config supplied before the Rust core handles +/// product calls. One core instance represents one product identity. +/// +/// `hostName`, `hostIcon`, `hostVersion`, `platformType`, and +/// `platformVersion` describe the host to the wallet during SSO pairing. +/// `peopleChainGenesisHash` must be exactly 32 bytes. +public struct RuntimeConfig: Sendable { + public let productId: String + public let hostName: String + public let hostIcon: String? + public let hostVersion: String? + public let platformType: String? + public let platformVersion: String? + public let peopleChainGenesisHash: Data + public let pairingDeeplinkScheme: PairingDeeplinkScheme + + public init( + productId: String, + hostName: String, + hostIcon: String? = nil, + hostVersion: String? = nil, + platformType: String? = nil, + platformVersion: String? = nil, + peopleChainGenesisHash: Data, + pairingDeeplinkScheme: PairingDeeplinkScheme = .polkadotApp + ) { + self.productId = productId + self.hostName = hostName + self.hostIcon = hostIcon + self.hostVersion = hostVersion + self.platformType = platformType + self.platformVersion = platformVersion + self.peopleChainGenesisHash = peopleChainGenesisHash + self.pairingDeeplinkScheme = pairingDeeplinkScheme + } + + fileprivate var native: NativeRuntimeConfig { + NativeRuntimeConfig( + productId: productId, + hostName: hostName, + hostIcon: hostIcon, + hostVersion: hostVersion, + platformType: platformType, + platformVersion: platformVersion, + peopleChainGenesisHash: peopleChainGenesisHash, + pairingDeeplinkScheme: pairingDeeplinkScheme.native + ) + } +} + +/// Bootstrap helper for the native localhost WebSocket bridge that the Rust +/// core stands up via `NativeTrUApiCore.startWsBridge(bindPort:)` when the +/// cdylib is built with the `ws-bridge` feature. +public enum LocalhostBridgeBootstrap { + /// Returns a `