Add mobile host scaffolding#215
Open
pgherveou wants to merge 274 commits into
Open
Conversation
Harden the Rust core runtime and host SDKs against the review of the core port. No change to the wire protocol or the generated artifacts (codegen regenerates byte-identically; the committed WASM bundle is rebuilt). truapi-server: - Subscriptions use generation-stamped reservation slots: a reused or raced request_id stops-and-replaces instead of leaking an unstoppable stream, and a _stop arriving before activation cancels the pending subscription. Unregistered methods answer Unsupported instead of leaving the caller to hang. Dead negotiated_version slot and interrupt API removed. - chainHead-v1 runtime: single-flight follow setup (no duplicate or leaked remote subscriptions), the close-vs-insert race in request_value is closed so a caller can no longer hang forever, the pending follow-event buffer is bounded, and an orphaned remote follow is unfollowed when its local follow is torn down mid-setup. - WS bridge: constant-time token comparison, bounded inbound message size, bounded outbound queue and connection count, documented loopback trust model. - wasm surface registers a panic hook and surfaces non-boolean / non-string host returns as errors; permissions persist only genuine user decisions, so a transient prompt-callback error no longer locks a capability out; dotns classifies trailing-dot FQDNs and lowercases/de-dupes remote domains; navigate_to returns an error instead of panicking on a contract violation. - smoldot provider is wired into the runtime behind its feature (with a platform fallback), with finite json-rpc ceilings, a distinguishable reconnect error, and Apache-2.0 attribution (SPDX headers + THIRD_PARTY_NOTICES.md). - Tests: subscription-race regressions, malformed-frame drop, subscription lifecycle through the wire boundary, and a hardened Rust<->TS wire-table parity check (panics on unparseable ids, compares all four subscription ids, asserts a row-count floor). codegen: Rust dispatcher/wire-table emitters use writedoc!/formatdoc! (byte-identical output); TS host-callbacks handles bare trait-object returns. JS hosts: worker-runtime faults now reach close subscribers; the WebSocket provider is built on the shared close-once base provider; the electron provider detaches port listeners on remote close; new tests for the node WASM round-trip, WS error/non-binary/CLOSING paths, and the iframe handshake/origin/dispose paths. native: Android and iOS HostBridge document that callbacks fire on the truapi-ws-bridge worker thread; the iOS bootstrap uses a JSONEncoder-based JS string-literal escaper; Maven coordinates reconciled across build.gradle.kts, jitpack.yml, and the README. CI/docs: host-packages CI runs the host-shared smoke test against the freshly built WASM bundle and triggers on truapi-platform changes; the Makefile wasm target no longer hides the committed bundle; design docs and the truapi-platform README are reconciled to the shipped trait set.
Native hosts only use the localhost WebSocket bridge, where the core writes
outbound frames straight to the socket, so the direct
receiveFromProduct/onCoreResponse delivery path was dead weight on the UniFFI
surface.
- truapi-server: remove HostCallbacks::on_core_response, NativeCallbackTransport,
and NativeTrUApiCore::{receive_from_product, debug_smoke_feature_request_frame}.
- Regenerate the UniFFI Kotlin + Swift bindings to drop those symbols.
- Android + iOS shells: drop HostBridge.onCoreResponse and the
receiveFromProduct / debugSmokeFeatureRequestFrame wrappers; trim the README
examples.
Web (WasmTrUApiCore.emitFrame / receiveFromProduct) is unchanged — it is the
primary delivery path there — as is the in-process
TrUApiCore::receive_from_product convenience used by the tests.
Merge @parity/truapi-host-{shared,web,electron} into one package,
@parity/truapi-host-wasm, with tree-shakeable subpath entries:
. core: createWasmProvider, createNodeWasmProvider, createHostServer, types
./web createIframeHost, createWebWorkerProvider
./electron createElectronProvider
./node-runtime · ./worker-runtime · ./wasm/{web,node}
@parity/truapi-host (no shared core) is unchanged. The ./web and ./electron
entries are side-effect-free and statically pull no core/node-runtime/wasm at
the value level, so bundlers tree-shake them; the `sideEffects` field scopes
the genuinely-effectful worker entry and the wasm glue. The wasm subpath
exports carry a `types` condition for typed consumers.
Updates codegen.sh (--platform-ts-output + prettier + echoes), the Makefile,
host-packages.yml (path filters + jobs), the committed WASM bundle location,
README/CLAUDE.md, the design docs, and package-lock.json to the new package.
590abd1 to
4c04a81
Compare
…' into worktree-issue-96-mobile-followup
Add a `truapi-host` binary with three roles that replace the external signing-bot for local end-to-end testing: - `relay`: an in-memory statement-store the two hosts pair over. - `pairing-host`: a seedless host (PairingHostRuntime) that presents a pairing deeplink and bridges product byte-frames over WebSocket. - `signing-host`: a wallet-local host (SigningHostRuntime) that answers the handshake and auto-signs, driven by the new SSO responder. truapi-server gains the §B responder half and the signing-host operations it needs: encode-side handshake helpers and responder codecs in `host_logic/sso/`, v4 extrinsic assembly (`host_logic/transaction.rs`), `runtime/signing_host/sso_responder.rs`, and `sign_payload` / `create_transaction` on the signing host. Product accounts and `rootUserAccountId` derive from `//wallet` (host-spec C.0/C.5) via a new sr25519 hard-junction helper. Bandersnatch ring-VRF product-account aliases (`get_account_alias`) use the `verifiable` crate, and lite-username attestation (`host_logic/attestation.rs` + CLI) registers a People-chain username so `get_user_id` resolves; the pairing host can resolve usernames from the real People chain while SSO stays on the relay (`identity_chain_genesis_hash`). The bun e2e driver (`e2e/`) runs the playground's own generated example sources against the pairing host and writes `explorer/diagnosis-reports/ headless.md`. With `E2E_LIVE_CHAIN=1` and a registered signer account it matches the browser host on every method it passes (43/44; the lone failure is a chain example that sends a deliberately-invalid operation id).
The stop_transaction playground example used a hardcoded placeholder operationId; it now broadcasts a transaction and stops it with the returned id, the correct usage. Also emit captured example output for passing methods in the headless diagnosis report and load the e2e signer mnemonic from a gitignored e2e/.env. Note: against the public paseo Asset Hub RPC, transaction_v1_stop rejects broadcast operation ids (-32602), so this method stays red there; it passes on hosts whose transaction API honors stop (e.g. smoldot in the browser host).
…' into headless-host
The headless hosts now connect to the statement store the same way an iOS/web client does: the real paseo-next-v2 People chain, which serves both the statement-store RPC and the chain RPC over one WebSocket. The in-memory relay (relay.rs, the `relay` subcommand, and the relay orchestration in the e2e runner) is removed; both hosts default `--statement-store` to the People node. Statement-store allowance is obtained on-chain. New native module `src/alloc/` ports `Resources.set_statement_store_account` (pallet 63 call 10): metadata-driven signed-extension encoding + inherited-implication proof message (pinned to the live-verified known answer), bandersnatch ring-VRF membership proof via the `verifiable` prover, the unsigned General (v5) extrinsic, `Members` ring fetch, SSS_SLOT slot scan, a minimal WS JSON-RPC client, and `author_submitAndWatchExtrinsic`. Before pairing, the signing host registers allowance for both its own `//wallet//sso` account and the pairing host's device key, proving its LitePeople ring membership. A new `alloc-check` subcommand diagnoses (or `--submit`s) allowance. statement_store_rpc::submit now inspects the SubmitResult: only `new`/ `known` count as accepted, so `NoAllowance`/`BadProof` rejections surface instead of being silently dropped.
…ations
The pairing host is now driven by a product script: `pairing-host --script
foo.ts --product-id p.dot [--auto-accept]` starts the frame server and runs the
script with a global `truapi` injected (the `@parity/truapi` client, scoped to
the product) plus a `host` helper. The Rust CLI spawns the script via an
embedded `js/runner.ts`, and the command exits with the script's status, so the
host command is the test — no separate bun orchestrator.
Both hosts take `--auto-accept`; without it every confirmation a web/iOS host
would show as a modal is prompted y/n on the CLI (ApprovalPolicy::Prompt).
`make headless` builds the CLI + client; `e2e/run.sh` is a thin launcher that
runs a product script through a pairing host and pipes the emitted deeplink to a
signing host. Product scripts live in `js/scripts/`: `battery.ts` (curated
signer gate, 7/7) and `diagnosis.ts` (playground generated examples ->
explorer/diagnosis-reports/headless.md, 43/1/20 with a live chain). The former
`e2e/{run-e2e,driver,diagnosis,ws-provider}.ts` and `wait-and-sign.sh` are gone.
Product ids must be a `.dot`/`localhost` identifier; the default is
`headless-playground.dot`.
Product scripts are now top-level code (no `export default` wrapper); the runner
injects `truapi` and a minimal `host` (`productId` + `productAccount(i)` only,
so accounts stay in sync with `--product-id`). Scripts use `console.log`/`throw`
for everything else. `js/scripts/{battery,diagnosis}.ts` converted accordingly.
The signing host reads its wallet mnemonic from `--mnemonic`, else the
`HOST_CLI_SIGNER_MNEMONIC` env var (or a gitignored `e2e/.env`), else the dev
mnemonic.
Allowance registration now waits for the `set_statement_store_account` extrinsic
to finalize, not just reach a block: the statement store does not honor a freshly
-set allowance at `inBlock`, so the handshake submit raced it and failed with
`NoAllowance`. Verified end-to-end against paseo-next-v2 (7/7 signer battery).
Move the entire Bulletin TransactionStorage.store submission into
truapi-server. The core builds the extrinsic offline (subxt 0.50.2), signs
it with the wallet-delegated allowance key, dry-runs it, broadcasts, and
watches for inclusion over the existing chainHead runtime — replacing the
host signer-callback seam so the allowance secret never crosses the
host/FFI boundary.
Core:
- host_logic/extrinsic.rs: offline SubstrateConfig assembler with a
config-pinned genesis hash, an sr25519 Signer, and metadata / transaction
validity / events / header decoders.
- host_logic/bulletin.rs: store{data} construction signed with the allowance
key, with audited pallet/call-index pinning plus a canonical-bytes guard so
provider metadata cannot redirect the signature, and a memcpy call-data
encoder that avoids scale-encode's per-byte cost.
- runtime/bulletin_rpc.rs: serialized submit flow (ephemeral with_runtime
follow, metadata, nonce, validate_transaction dry-run, broadcast, single
event-loop inclusion watch gated on nonce advance, System.Events dispatch
check), typed error taxonomy, and broadcast stop on every exit.
- runtime.rs: Preimage::submit gates on bulletin availability before any
prompt and refreshes the allowance (Increase policy) with one retry on an
allowance rejection; lookup_subscribe verifies blake2_256(value)==key and
serves an in-core content-addressed cache.
- BulletinAllowanceKey is zeroized on drop; PreimageHost keeps only
lookup_preimage; both host configs gain an optional Bulletin genesis hash.
Codegen/TS: regenerate goldens (product wire unchanged) and drop the signer
bridge from the handwritten host worker. CI compiles the crate for
wasm32-unknown-unknown; .gitignore ignores the renamed wasm bundle path.
Bumps the hosts/dotli gitlink to the matching submodule commit.
Adversarial review of the watch loop found two defects: - A crafted or buggy chain provider could send a self-referential or cyclic `NewBlock` parent link. The nonce-advance ancestor walk had no visited-set guard and no `.await`, so such a link spun forever, freezing the worker and permanently holding the submit lock across all products. The walk is extracted into `ancestors_to_check`, which guards against self-parent and cyclic links with a visited set, and is unit-tested. - Blocks that failed the nonce gate were marked checked but never unpinned, leaking chainHead pins over the watch's lifetime and risking a false BroadcastUnverified once the server's pin limit was hit. They are now unpinned like body-negative blocks. Also log a warning when a host lookup value is downgraded to a miss for failing the blake2_256(value)==key integrity check.
…bler The signing-host role now builds and signs transactions locally instead of returning Unavailable, reusing host_logic/extrinsic.rs. Because ProductAccountTxPayload carries each extension's `extra` and `additional_signed` already SCALE-encoded in canonical order, assembly is a pure offline concatenation — no metadata, no RPC: - extrinsic.rs gains build_signed_extrinsic_v4 + v4_signer_payload and an Sr25519Signer::from_keypair constructor. Body = Compact(len) ++ 0x84 ++ MultiAddress::Id(signer) ++ MultiSignature::Sr25519(sig) ++ Σextra ++ call_data; signer payload = call_data ++ Σextra ++ Σadditional_signed, blake2_256 only when >256 bytes. Layout is byte-identical to subxt / frame-decode. - signing_host::create_transaction handles Product and LegacyAccount (with a fail-closed slot-zero key-match check); the product-facing entrypoint's caller-scoping, chain-submit permission, and user-confirmation gates already precede it. - Extrinsic V5 (tx_ext_version != 0) returns the new AuthorityError::NotSupported -> HostCreateTransactionError::NotSupported: V5 general carries the signature inside a VerifySignature extension, which cannot come from pre-encoded parts. Tested: v4 layout + signature verification, the >256 hashing boundary, extension order preservation, and Product/LegacyAccount success plus v5/mismatch/no-session rejections.
Resolve the headless-host conflicts, add statement-store allowance allocation for SSO, and refresh the headless diagnosis path.
Decode ring revisions correctly for Bulletin allowance claims. Add CLI-managed signer accounts, network presets, and script-capable host modes. Regenerate headless signing and pairing diagnosis reports with zero failures.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Verification