From 71fdbc69f68ae9a920eb62593af0516aafb65916 Mon Sep 17 00:00:00 2001 From: w Date: Tue, 30 Jun 2026 16:12:21 -0400 Subject: [PATCH 1/2] feat: add Rust host runtime boundary --- CLAUDE.md | 4 +- Cargo.lock | 131 ++ README.md | 4 +- rust/crates/truapi-codegen/Cargo.toml | 3 + rust/crates/truapi-codegen/src/main.rs | 14 + rust/crates/truapi-codegen/src/rust.rs | 575 +++++++ .../truapi-codegen/src/rust/dispatcher.rs | 461 ++++++ .../truapi-codegen/src/rust/wire_table.rs | 296 ++++ .../truapi-codegen/tests/golden/dispatcher.rs | 1139 +++++++++++++ .../truapi-codegen/tests/golden/wire_table.rs | 722 ++++++++ .../truapi-codegen/tests/golden_rust_emit.rs | 148 ++ rust/crates/truapi-server/Cargo.toml | 12 + rust/crates/truapi-server/src/dispatcher.rs | 310 ++++ rust/crates/truapi-server/src/frame.rs | 436 +++++ .../truapi-server/src/generated/dispatcher.rs | 1468 +++++++++++++++++ .../crates/truapi-server/src/generated/mod.rs | 4 + .../truapi-server/src/generated/wire_table.rs | 722 ++++++++ rust/crates/truapi-server/src/lib.rs | 18 + rust/crates/truapi-server/src/subscription.rs | 495 ++++++ rust/crates/truapi-server/src/transport.rs | 12 + .../truapi-server/tests/golden_frame.rs | 53 + .../tests/snapshots/golden-account-get.bin | Bin 0 -> 14 bytes .../tests/wire_table_ts_parity.rs | 228 +++ scripts/codegen.sh | 5 + 24 files changed, 7257 insertions(+), 3 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/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-server/Cargo.toml 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/lib.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/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_table_ts_parity.rs diff --git a/CLAUDE.md b/CLAUDE.md index 0116c828..03a5659e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,6 +11,7 @@ rust/crates/ truapi/ Rust trait + type definitions for protocol versions v0.1 and v0.2 truapi-codegen/ rustdoc JSON → TypeScript client + Rust dispatcher truapi-macros/ #[wire(id = N)] proc-macro + truapi-server/ host runtime: frames, dispatcher, subscriptions js/packages/ truapi/ @parity/truapi TS package; generated TS lives under ignored paths playground/ Next.js interactive playground; deploys to truapi-playground.dot @@ -54,7 +55,8 @@ When the Rust trait surface changes, rerun: ``` That will repopulate the ignored generated TS under `js/packages/truapi/src/generated/`, -`js/packages/truapi/src/playground/codegen/`, and `js/packages/truapi/test/generated/examples/`. +`js/packages/truapi/src/playground/codegen/`, `js/packages/truapi/test/generated/examples/`, +and the checked-in Rust dispatcher under `rust/crates/truapi-server/src/generated/`. After regenerating, rebuild the client and refresh the playground's link copy: ```bash diff --git a/Cargo.lock b/Cargo.lock index 515ec10f..eba92e34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,6 +64,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + [[package]] name = "bitvec" version = "1.0.1" @@ -82,6 +88,12 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" +[[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" @@ -196,6 +208,22 @@ 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 = "funty" version = "2.0.0" @@ -290,6 +318,17 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + [[package]] name = "hashbrown" version = "0.17.0" @@ -365,12 +404,30 @@ version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[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" @@ -438,6 +495,12 @@ 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" @@ -453,6 +516,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -537,6 +613,19 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -567,6 +656,37 @@ dependencies = [ "winnow", ] +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + [[package]] name = "truapi" version = "0.3.1" @@ -588,6 +708,7 @@ dependencies = [ "indoc", "serde", "serde_json", + "tempfile", "truapi", ] @@ -600,6 +721,16 @@ dependencies = [ "syn", ] +[[package]] +name = "truapi-server" +version = "0.1.0" +dependencies = [ + "futures", + "parity-scale-codec", + "tracing", + "truapi", +] + [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/README.md b/README.md index a08d9483..d8755190 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ rust/crates/ truapi/ Rust trait and type definitions (v01, v02) truapi-codegen/ rustdoc JSON to TypeScript client + Rust dispatcher truapi-macros/ #[wire(id = N)] proc-macro + truapi-server/ host runtime: frames, dispatcher, subscriptions js/packages/ truapi/ @parity/truapi TypeScript client playground/ Interactive Next.js playground (truapi-playground.dot) @@ -68,7 +69,7 @@ scripts/codegen.sh Regenerate the TS client from the Rust source ## How it works 1. The protocol is defined as Rust traits in [`rust/crates/truapi/`](rust/crates/truapi/), with each method tagged `#[wire(id = N)]` for a stable byte-level dispatch table. Every method's doc comment must carry a ` ```ts ` example, which codegen extracts into the playground's EXAMPLE tab; the build fails if any method is missing one. -2. `truapi-codegen` reads rustdoc JSON for that crate and generates the TypeScript client under git-ignored paths in `js/packages/truapi/`. +2. `truapi-codegen` reads rustdoc JSON for that crate and generates the TypeScript client under git-ignored paths in `js/packages/truapi/`, plus the Rust dispatcher and wire table consumed by `truapi-server`. 3. Higher-level SDKs wrap the typed client; the transport encodes SCALE frames and ships them over `MessagePort` (or `postMessage` in iframe mode) to the host. 4. The host decodes the frame, dispatches to the matching trait method, encodes the response, and ships it back. @@ -129,4 +130,3 @@ See [`CONTRIBUTING.md`](CONTRIBUTING.md) for issue reports, feature proposals, a ## License [MIT](./LICENSE) - diff --git a/rust/crates/truapi-codegen/Cargo.toml b/rust/crates/truapi-codegen/Cargo.toml index c6d3ef3c..c376346e 100644 --- a/rust/crates/truapi-codegen/Cargo.toml +++ b/rust/crates/truapi-codegen/Cargo.toml @@ -17,3 +17,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 0dfa318c..a2d46418 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; @@ -43,6 +45,13 @@ struct Cli { #[arg(long)] client_examples_output: Option, + /// Output directory for the generated Rust dispatcher and wire table (optional). + /// + /// When set, emits `dispatcher.rs` and `wire_table.rs` for the host runtime + /// crate to include. + #[arg(long)] + rust_output: Option, + /// Output directory for generated explorer metadata (optional). When set, /// writes `codegen/types.ts` with the DataType list consumed by the /// explorer site. @@ -111,6 +120,11 @@ fn main() -> Result<()> { .with_context(|| format!("writing client examples to {path}"))?; println!("Generated client examples in {path}"); } + if let Some(path) = &cli.rust_output { + rust::generate(&api, path) + .with_context(|| format!("writing Rust dispatcher to {}", path.display()))?; + println!("Generated Rust dispatcher in {}", path.display()); + } if let Some(path) = &cli.explorer_output { ts::generate_explorer(&api, path, client_version) .with_context(|| format!("writing explorer metadata to {path}"))?; diff --git a/rust/crates/truapi-codegen/src/rust.rs b/rust/crates/truapi-codegen/src/rust.rs new file mode 100644 index 00000000..4b55b977 --- /dev/null +++ b/rust/crates/truapi-codegen/src/rust.rs @@ -0,0 +1,575 @@ +//! 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. + +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) +} + +/// The `SCREAMING_SNAKE_CASE` const name holding a wire method's ids. +pub(crate) fn const_name(wire_method: &str) -> String { + wire_method.to_uppercase() +} + +/// Const name for a trait/method pair's wire ids. The single naming +/// algorithm shared by the Rust and TS wire-table emitters, so both +/// generated surfaces agree on const names. +#[cfg(test)] +pub(crate) fn wire_const_name(trait_name: &str, method_name: &str) -> String { + const_name(&wire_method_name(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)> { + // Each method's ids are emitted as a named const, e.g. + // pub const PREIMAGE_SUBMIT: RequestFrameIds = RequestFrameIds { + // request_id: 68, + // response_id: 69, + // }; + // Reconstruct the `(id, "{method}_{suffix}")` pairs the assertions use, + // mirroring the parser in `tests/wire_table_ts_parity.rs`. + let mut out = Vec::new(); + let mut lines = src.lines(); + while let Some(line) = lines.next() { + let Some(rest) = line.trim().strip_prefix("pub const ") else { + continue; + }; + let Some(colon) = rest.find(':') else { + continue; + }; + let is_sub = rest.contains("SubscriptionFrameIds"); + // Skip non-id consts (e.g. `WIRE_TABLE: &[WireEntry]`). + if !is_sub && !rest.contains("RequestFrameIds") { + continue; + } + let method = rest[..colon].trim().to_ascii_lowercase(); + + let mut ids: std::collections::BTreeMap<&str, u8> = std::collections::BTreeMap::new(); + for inner in lines.by_ref() { + let t = inner.trim(); + if t.starts_with("};") { + break; + } + if let Some((field, val)) = t.split_once(':') { + let id = val.trim().trim_end_matches(',').parse::().unwrap(); + ids.insert(field.trim(), id); + } + } + + let suffixes: &[(&str, &str)] = if is_sub { + &[ + ("start_id", "start"), + ("stop_id", "stop"), + ("interrupt_id", "interrupt"), + ("receive_id", "receive"), + ] + } else { + &[("request_id", "request"), ("response_id", "response")] + }; + for (field, suffix) in suffixes { + out.push((ids[field], format!("{method}_{suffix}"))); + } + } + 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(), + module_path: Vec::new(), + 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(), + module_path: Vec::new(), + methods: vec![make_request_method("submit", 62)], + docs: None, + }, + TraitDef { + name: "Preimage".to_string(), + module_path: Vec::new(), + 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("wire_table::STATEMENT_STORE_SUBMIT"), + "dispatcher missing prefixed StatementStore const:\n{dispatcher}" + ); + assert!( + dispatcher.contains("wire_table::PREIMAGE_SUBMIT"), + "dispatcher missing prefixed Preimage const:\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(), + module_path: Vec::new(), + methods: vec![make_request_method("bar_baz", 10)], + docs: None, + }, + TraitDef { + name: "FooBar".to_string(), + module_path: Vec::new(), + 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(), + module_path: Vec::new(), + 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(), + module_path: Vec::new(), + 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}", + ); + } + + /// `wire_const_name` is the single naming algorithm for wire-id consts; + /// the TS and Rust emitters both call it. Pin its behavior on digits + /// (kept attached) and consecutive capitals (split per letter) so the + /// two generated surfaces can never drift apart. + #[test] + fn wire_const_name_pins_digits_and_acronyms() { + assert_eq!(wire_const_name("Preimage", "submit"), "PREIMAGE_SUBMIT"); + assert_eq!(wire_const_name("Signing", "sign_v2"), "SIGNING_SIGN_V2"); + assert_eq!( + wire_const_name("HTTPServer", "serve"), + "H_T_T_P_SERVER_SERVE" + ); + assert_eq!( + wire_const_name("StatementStore", "create_proof"), + "STATEMENT_STORE_CREATE_PROOF" + ); + } + + #[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"); + } + + /// 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(), + module_path: Vec::new(), + 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(), + module_path: Vec::new(), + 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(), + module_path: Vec::new(), + 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(), + module_path: Vec::new(), + 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(), + module_path: Vec::new(), + 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(), + module_path: Vec::new(), + 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-codegen/src/rust/dispatcher.rs b/rust/crates/truapi-codegen/src/rust/dispatcher.rs new file mode 100644 index 00000000..5989acfa --- /dev/null +++ b/rust/crates/truapi-codegen/src/rust/dispatcher.rs @@ -0,0 +1,461 @@ +//! 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 codegen itself +//! does not compile the output; string-diff golden tests guard it. + +use std::collections::BTreeMap; +use std::collections::BTreeSet; +use std::fmt::Write; + +use anyhow::{Result, bail}; +use indoc::{formatdoc, indoc, writedoc}; + +use crate::rustdoc::*; + +use super::{const_name, 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 trait_name = &trait_def.name; + let mut code = String::new(); + writedoc!( + code, + r#" + fn {fn_name}

(dispatcher: &mut Dispatcher, host: Arc

) + where + P: {trait_name} + Send + Sync + 'static, + {{ + "# + ) + .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}`); uppercased + /// to the `wire_table` const this method registers against. + 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: method.kind, + 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 ids = const_name(&self.wire_name); + + write_indented( + out, + 4, + &formatdoc! { + r#" + {{ + let host = {host_expr}; + dispatcher.on_request(wire_table::{ids}, move |request_id: String, bytes: Vec| {{ + let host = host.clone(); + Box::pin(async move {{ + "# + }, + ); + let call_args = if let Some(request) = &self.request_wrapper { + write_indented( + out, + 16, + &formatdoc! { + r#" + let request: versioned::{module}::{request} = + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; + "# + }, + ); + "&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) => write_indented( + out, + 16, + &formatdoc! { + r#" + let response: versioned::{module}::{response} = match host.{method}({call_args}).await {{ + Ok(value) => value, + Err(err) => return Err(encode_call_error_payload(err)), + }}; + Ok(encode_ok_payload(response)) + "# + }, + ), + None => write_indented( + out, + 16, + &formatdoc! { + r#" + match host.{method}({call_args}).await {{ + Ok(()) => Ok(encode_ok_payload(())), + Err(err) => Err(encode_call_error_payload(err)), + }} + "# + }, + ), + } + write_indented( + out, + 4, + indoc! { + r#" + }) + }); + } + "# + }, + ); + } + + fn write_subscription(&self, out: &mut String, host_expr: &str) { + let module = &self.module; + let method = &self.name; + let ids = const_name(&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); + + write_indented( + out, + 4, + &formatdoc! { + r#" + {{ + let host = {host_expr}; + dispatcher.on_subscription(wire_table::{ids}, move |request_id: String, bytes: Vec| {{ + let host = host.clone(); + Box::pin(async move {{ + "# + }, + ); + let call_args = if let Some(request) = &self.request_wrapper { + write_indented( + out, + 16, + &formatdoc! { + r#" + let request: versioned::{module}::{request} = + Decode::decode(&mut &bytes[..]).map_err(|e| encode_decode_error(e.to_string()))?; + "# + }, + ); + "&cx, request" + } else { + writeln!(out, " let _ = bytes;").unwrap(); + "&cx" + }; + writeln!( + out, + " let cx = CallContext::with_request_id(request_id.clone());" + ) + .unwrap(); + if is_result_sub { + write_indented( + out, + 16, + &formatdoc! { + r#" + let stream = match host.{method}({call_args}).await {{ + Ok(sub) => sub, + Err(err) => return Err(encode_call_error_payload(err)), + }}; + "# + }, + ); + } else { + writeln!( + out, + " let stream = host.{method}({call_args}).await;" + ) + .unwrap(); + } + writeln!( + out, + " Ok(subscription_stream::(stream))" + ) + .unwrap(); + write_indented( + out, + 4, + indoc! { + r#" + }) + }); + } + "# + }, + ); + } +} + +fn named_root(ty: &TypeRef) -> Option { + if let TypeRef::Named { name, args } = ty + && args.is_empty() + { + return Some(name.clone()); + } + None +} + +/// Append `block` to `out`, prefixing every non-empty line with `indent` spaces. +fn write_indented(out: &mut String, indent: usize, block: &str) { + let pad = " ".repeat(indent); + for line in block.lines() { + if line.is_empty() { + out.push('\n'); + } else { + writeln!(out, "{pad}{line}").unwrap(); + } + } +} + +fn write_header(out: &mut String) { + writedoc!( + out, + r#" + //! Wire dispatcher for the unified `TrUApi` trait. + //! + //! Auto-generated by truapi-codegen. Do not edit. + + "# + ) + .unwrap(); +} + +fn write_imports(out: &mut String, traits: &[&TraitDef]) { + writedoc!( + out, + r#" + use std::sync::Arc; + + use parity_scale_codec::Decode; + + use truapi::CallContext; + use truapi::api::{{ + "# + ) + .unwrap(); + for trait_def in traits { + writeln!(out, " {},", trait_def.name).unwrap(); + } + writedoc!( + out, + r#" + }}; + use truapi::versioned; + + use crate::dispatcher::Dispatcher; + use crate::frame::{{encode_call_error_payload, encode_decode_error, encode_ok_payload}}; + use crate::generated::wire_table; + use crate::subscription::subscription_stream; + "# + ) + .unwrap(); +} + +fn write_top_register(out: &mut String, traits: &[&TraitDef]) { + let trait_names: Vec<&str> = traits.iter().map(|t| t.name.as_str()).collect(); + let bounds = trait_names.join(" + "); + writedoc!( + out, + r#" + /// Register every TrUAPI method with the dispatcher. + pub fn register

(dispatcher: &mut Dispatcher, host: Arc

) + where + P: {bounds} + Send + Sync + 'static, + {{ + "# + ) + .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..8696b575 --- /dev/null +++ b/rust/crates/truapi-codegen/src/rust/wire_table.rs @@ -0,0 +1,296 @@ +//! 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 indoc::{formatdoc, writedoc}; + +use crate::rustdoc::*; + +use super::{const_name, 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) +} + +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)]) -> Result { + let mut out = String::new(); + writedoc!( + out, + r#" + //! 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 ids for each method are exposed + //! as a named const (`PREIMAGE_SUBMIT`, ...); [`WIRE_TABLE`] and the + //! generated dispatcher both reference those consts so the numbers live + //! in exactly one place. The table is sorted by request/start id. + + /// Request method wire discriminants. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct RequestFrameIds {{ + /// Discriminant for the request frame. + pub request_id: u8, + /// Discriminant for the response frame. + pub response_id: u8, + }} + + /// Subscription method wire discriminants. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct SubscriptionFrameIds {{ + /// Discriminant for the start frame. + pub start_id: u8, + /// Discriminant for the stop frame. + pub stop_id: u8, + /// Discriminant for the interrupt frame (server-initiated termination). + pub interrupt_id: u8, + /// Discriminant for each receive frame (a streamed item). + pub receive_id: u8, + }} + + /// A single wire-table row. + pub struct WireEntry {{ + /// Method name from the Rust trait. + pub method: &'static str, + /// What kind of slot this entry describes. + pub kind: WireKind, + }} + + /// Wire-slot shape: request/response pair or subscription quartet. + pub enum WireKind {{ + /// Request/response method. + Request(RequestFrameIds), + /// Subscription method. + Subscription(SubscriptionFrameIds), + }} + "# + ) + .unwrap(); + + // Per-method consts: the single source of truth for each method's ids. + for (name, entry) in methods { + let konst = const_name(name); + let block = match entry { + MethodEntry::Request(WireEntry { + request_id, + response_id, + }) => formatdoc! { + r#" + /// Wire discriminants for `{name}`. + pub const {konst}: RequestFrameIds = RequestFrameIds {{ + request_id: {request_id}, + response_id: {response_id}, + }}; + "# + }, + MethodEntry::Subscription(SubEntry { + start_id, + stop_id, + interrupt_id, + receive_id, + }) => formatdoc! { + r#" + /// Wire discriminants for `{name}`. + pub const {konst}: SubscriptionFrameIds = SubscriptionFrameIds {{ + start_id: {start_id}, + stop_id: {stop_id}, + interrupt_id: {interrupt_id}, + receive_id: {receive_id}, + }}; + "# + }, + }; + out.push('\n'); + out.push_str(&block); + } + + out.push('\n'); + writedoc!( + out, + r#" + /// 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] = &[ + "# + ) + .unwrap(); + for (name, entry) in methods { + let konst = const_name(name); + let variant = match entry { + MethodEntry::Request(_) => "Request", + MethodEntry::Subscription(_) => "Subscription", + }; + let block = formatdoc! { + r#" + WireEntry {{ + method: "{name}", + kind: WireKind::{variant}({konst}), + }}, + "# + }; + for line in block.lines() { + writeln!(out, " {line}").unwrap(); + } + } + writeln!(out, "];").unwrap(); + + Ok(out) +} 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..510b094c --- /dev/null +++ b/rust/crates/truapi-codegen/tests/golden/dispatcher.rs @@ -0,0 +1,1139 @@ +//! Wire dispatcher for the unified `TrUApi` trait. +//! +//! Auto-generated by truapi-codegen. Do not edit. + +use std::sync::Arc; + +use parity_scale_codec::Decode; + +use truapi::CallContext; +use truapi::api::{ + Account, + Chain, + Chat, + CoinPayment, + Entropy, + LocalStorage, + Notifications, + Payment, + Permissions, + Preimage, + ResourceAllocation, + Signing, + StatementStore, + System, + Theme, +}; +use truapi::versioned; + +use crate::dispatcher::Dispatcher; +use crate::frame::{encode_call_error_payload, encode_decode_error, encode_ok_payload}; +use crate::generated::wire_table; +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 + CoinPayment + Entropy + LocalStorage + Notifications + 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_coin_payment(dispatcher, host.clone()); + register_entropy(dispatcher, host.clone()); + register_local_storage(dispatcher, host.clone()); + register_notifications(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(wire_table::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(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } +} + +fn register_chain

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Chain + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::CHAIN_FOLLOW_HEAD_SUBSCRIBE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadFollowRequest = + 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)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } +} + +fn register_chat

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Chat + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::CHAT_LIST_SUBSCRIBE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let _ = 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(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::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(wire_table::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| 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)) + }) + }); + } +} + +fn register_coin_payment

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: CoinPayment + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request(wire_table::COIN_PAYMENT_CREATE_PURSE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentCreatePurseRequest = + 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::coin_payment::HostCoinPaymentCreatePurseResponse = match host.create_purse(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::COIN_PAYMENT_QUERY_PURSE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentQueryPurseRequest = + 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::coin_payment::HostCoinPaymentQueryPurseResponse = match host.query_purse(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::COIN_PAYMENT_REBALANCE_PURSE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentRebalancePurseRequest = + 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.rebalance_purse(&cx, request).await { + Ok(sub) => sub, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::COIN_PAYMENT_DELETE_PURSE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentDeletePurseRequest = + 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.delete_purse(&cx, request).await { + Ok(sub) => sub, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::COIN_PAYMENT_CREATE_RECEIVABLE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentCreateReceivableRequest = + 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::coin_payment::HostCoinPaymentCreateReceivableResponse = match host.create_receivable(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::COIN_PAYMENT_CREATE_CHEQUE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentCreateChequeRequest = + 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::coin_payment::HostCoinPaymentCreateChequeResponse = match host.create_cheque(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::COIN_PAYMENT_DEPOSIT, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentDepositRequest = + 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.deposit(&cx, request).await { + Ok(sub) => sub, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::COIN_PAYMENT_REFUND, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentRefundRequest = + 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.refund(&cx, request).await { + Ok(sub) => sub, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host; + dispatcher.on_subscription(wire_table::COIN_PAYMENT_LISTEN_FOR_PAYMENT, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentListenForRequest = + 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.listen_for_payment(&cx, request).await { + Ok(sub) => sub, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(subscription_stream::(stream)) + }) + }); + } +} + +fn register_entropy

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Entropy + Send + Sync + 'static, +{ + { + let host = host; + dispatcher.on_request(wire_table::ENTROPY_DERIVE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::entropy::HostDeriveEntropyRequest = + 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } +} + +fn register_local_storage

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: LocalStorage + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request(wire_table::LOCAL_STORAGE_READ, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::local_storage::HostLocalStorageReadRequest = + 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } +} + +fn register_notifications

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Notifications + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request(wire_table::NOTIFICATIONS_SEND_PUSH_NOTIFICATION, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::notifications::HostPushNotificationRequest = + 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::notifications::HostPushNotificationResponse = match host.send_push_notification(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::notifications::HostPushNotificationCancelRequest = + 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::notifications::HostPushNotificationCancelResponse = match host.cancel_push_notification(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } +} + +fn register_payment

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Payment + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::PAYMENT_BALANCE_SUBSCRIBE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::payment::HostPaymentBalanceSubscribeRequest = + 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(encode_call_error_payload(err)), + }; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::PAYMENT_REQUEST, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::payment::HostPaymentRequest = + 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::HostPaymentResponse = match host.request(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::PAYMENT_STATUS_SUBSCRIBE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::payment::HostPaymentStatusSubscribeRequest = + 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(encode_call_error_payload(err)), + }; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } +} + +fn register_permissions

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Permissions + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request(wire_table::PERMISSIONS_REQUEST_DEVICE_PERMISSION, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::permissions::HostDevicePermissionRequest = + 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } +} + +fn register_preimage

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Preimage + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::PREIMAGE_LOOKUP_SUBSCRIBE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::preimage::RemotePreimageLookupSubscribeRequest = + 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)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } +} + +fn register_resource_allocation

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: ResourceAllocation + Send + Sync + 'static, +{ + { + let host = host; + dispatcher.on_request(wire_table::RESOURCE_ALLOCATION_REQUEST, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::resource_allocation::HostRequestResourceAllocationRequest = + 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } +} + +fn register_signing

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Signing + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request(wire_table::SIGNING_CREATE_TRANSACTION, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostCreateTransactionRequest = + 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } +} + +fn register_statement_store

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: StatementStore + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::STATEMENT_STORE_SUBSCRIBE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::statement_store::RemoteStatementStoreSubscribeRequest = + 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)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::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| encode_decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + match host.submit(&cx, request).await { + Ok(()) => Ok(encode_ok_payload(())), + Err(err) => Err(encode_call_error_payload(err)), + } + }) + }); + } +} + +fn register_system

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: System + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request(wire_table::SYSTEM_HANDSHAKE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::system::HostHandshakeRequest = + 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } +} + +fn register_theme

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Theme + Send + Sync + 'static, +{ + { + let host = host; + dispatcher.on_subscription(wire_table::THEME_SUBSCRIBE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let _ = 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..12a72a94 --- /dev/null +++ b/rust/crates/truapi-codegen/tests/golden/wire_table.rs @@ -0,0 +1,722 @@ +//! 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 ids for each method are exposed +//! as a named const (`PREIMAGE_SUBMIT`, ...); [`WIRE_TABLE`] and the +//! generated dispatcher both reference those consts so the numbers live +//! in exactly one place. The table is sorted by request/start id. + +/// Request method wire discriminants. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RequestFrameIds { + /// Discriminant for the request frame. + pub request_id: u8, + /// Discriminant for the response frame. + pub response_id: u8, +} + +/// Subscription method wire discriminants. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SubscriptionFrameIds { + /// Discriminant for the start frame. + pub start_id: u8, + /// Discriminant for the stop frame. + pub stop_id: u8, + /// Discriminant for the interrupt frame (server-initiated termination). + pub interrupt_id: u8, + /// Discriminant for each receive frame (a streamed item). + pub receive_id: u8, +} + +/// A single wire-table row. +pub struct WireEntry { + /// Method name from the Rust trait. + pub method: &'static str, + /// What kind of slot this entry describes. + pub kind: WireKind, +} + +/// Wire-slot shape: request/response pair or subscription quartet. +pub enum WireKind { + /// Request/response method. + Request(RequestFrameIds), + /// Subscription method. + Subscription(SubscriptionFrameIds), +} + +/// Wire discriminants for `system_handshake`. +pub const SYSTEM_HANDSHAKE: RequestFrameIds = RequestFrameIds { + request_id: 0, + response_id: 1, +}; + +/// Wire discriminants for `system_feature_supported`. +pub const SYSTEM_FEATURE_SUPPORTED: RequestFrameIds = RequestFrameIds { + request_id: 2, + response_id: 3, +}; + +/// Wire discriminants for `notifications_send_push_notification`. +pub const NOTIFICATIONS_SEND_PUSH_NOTIFICATION: RequestFrameIds = RequestFrameIds { + request_id: 4, + response_id: 5, +}; + +/// Wire discriminants for `system_navigate_to`. +pub const SYSTEM_NAVIGATE_TO: RequestFrameIds = RequestFrameIds { + request_id: 6, + response_id: 7, +}; + +/// Wire discriminants for `permissions_request_device_permission`. +pub const PERMISSIONS_REQUEST_DEVICE_PERMISSION: RequestFrameIds = RequestFrameIds { + request_id: 8, + response_id: 9, +}; + +/// Wire discriminants for `permissions_request_remote_permission`. +pub const PERMISSIONS_REQUEST_REMOTE_PERMISSION: RequestFrameIds = RequestFrameIds { + request_id: 10, + response_id: 11, +}; + +/// Wire discriminants for `local_storage_read`. +pub const LOCAL_STORAGE_READ: RequestFrameIds = RequestFrameIds { + request_id: 12, + response_id: 13, +}; + +/// Wire discriminants for `local_storage_write`. +pub const LOCAL_STORAGE_WRITE: RequestFrameIds = RequestFrameIds { + request_id: 14, + response_id: 15, +}; + +/// Wire discriminants for `local_storage_clear`. +pub const LOCAL_STORAGE_CLEAR: RequestFrameIds = RequestFrameIds { + request_id: 16, + response_id: 17, +}; + +/// Wire discriminants for `account_connection_status_subscribe`. +pub const ACCOUNT_CONNECTION_STATUS_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 18, + stop_id: 19, + interrupt_id: 20, + receive_id: 21, +}; + +/// Wire discriminants for `account_get_account`. +pub const ACCOUNT_GET_ACCOUNT: RequestFrameIds = RequestFrameIds { + request_id: 22, + response_id: 23, +}; + +/// Wire discriminants for `account_get_account_alias`. +pub const ACCOUNT_GET_ACCOUNT_ALIAS: RequestFrameIds = RequestFrameIds { + request_id: 24, + response_id: 25, +}; + +/// Wire discriminants for `account_create_account_proof`. +pub const ACCOUNT_CREATE_ACCOUNT_PROOF: RequestFrameIds = RequestFrameIds { + request_id: 26, + response_id: 27, +}; + +/// Wire discriminants for `account_get_legacy_accounts`. +pub const ACCOUNT_GET_LEGACY_ACCOUNTS: RequestFrameIds = RequestFrameIds { + request_id: 28, + response_id: 29, +}; + +/// Wire discriminants for `signing_create_transaction`. +pub const SIGNING_CREATE_TRANSACTION: RequestFrameIds = RequestFrameIds { + request_id: 30, + response_id: 31, +}; + +/// Wire discriminants for `signing_create_transaction_with_legacy_account`. +pub const SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { + request_id: 32, + response_id: 33, +}; + +/// Wire discriminants for `signing_sign_raw_with_legacy_account`. +pub const SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { + request_id: 34, + response_id: 35, +}; + +/// Wire discriminants for `signing_sign_payload_with_legacy_account`. +pub const SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { + request_id: 36, + response_id: 37, +}; + +/// Wire discriminants for `chat_create_room`. +pub const CHAT_CREATE_ROOM: RequestFrameIds = RequestFrameIds { + request_id: 38, + response_id: 39, +}; + +/// Wire discriminants for `chat_register_bot`. +pub const CHAT_REGISTER_BOT: RequestFrameIds = RequestFrameIds { + request_id: 40, + response_id: 41, +}; + +/// Wire discriminants for `chat_list_subscribe`. +pub const CHAT_LIST_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 42, + stop_id: 43, + interrupt_id: 44, + receive_id: 45, +}; + +/// Wire discriminants for `chat_post_message`. +pub const CHAT_POST_MESSAGE: RequestFrameIds = RequestFrameIds { + request_id: 46, + response_id: 47, +}; + +/// Wire discriminants for `chat_action_subscribe`. +pub const CHAT_ACTION_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 48, + stop_id: 49, + interrupt_id: 50, + receive_id: 51, +}; + +/// Wire discriminants for `chat_custom_message_render_subscribe`. +pub const CHAT_CUSTOM_MESSAGE_RENDER_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 52, + stop_id: 53, + interrupt_id: 54, + receive_id: 55, +}; + +/// Wire discriminants for `statement_store_subscribe`. +pub const STATEMENT_STORE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 56, + stop_id: 57, + interrupt_id: 58, + receive_id: 59, +}; + +/// Wire discriminants for `statement_store_create_proof`. +pub const STATEMENT_STORE_CREATE_PROOF: RequestFrameIds = RequestFrameIds { + request_id: 60, + response_id: 61, +}; + +/// Wire discriminants for `statement_store_submit`. +pub const STATEMENT_STORE_SUBMIT: RequestFrameIds = RequestFrameIds { + request_id: 62, + response_id: 63, +}; + +/// Wire discriminants for `preimage_lookup_subscribe`. +pub const PREIMAGE_LOOKUP_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 64, + stop_id: 65, + interrupt_id: 66, + receive_id: 67, +}; + +/// Wire discriminants for `preimage_submit`. +pub const PREIMAGE_SUBMIT: RequestFrameIds = RequestFrameIds { + request_id: 68, + response_id: 69, +}; + +/// Wire discriminants for `chain_follow_head_subscribe`. +pub const CHAIN_FOLLOW_HEAD_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 76, + stop_id: 77, + interrupt_id: 78, + receive_id: 79, +}; + +/// Wire discriminants for `chain_get_head_header`. +pub const CHAIN_GET_HEAD_HEADER: RequestFrameIds = RequestFrameIds { + request_id: 80, + response_id: 81, +}; + +/// Wire discriminants for `chain_get_head_body`. +pub const CHAIN_GET_HEAD_BODY: RequestFrameIds = RequestFrameIds { + request_id: 82, + response_id: 83, +}; + +/// Wire discriminants for `chain_get_head_storage`. +pub const CHAIN_GET_HEAD_STORAGE: RequestFrameIds = RequestFrameIds { + request_id: 84, + response_id: 85, +}; + +/// Wire discriminants for `chain_call_head`. +pub const CHAIN_CALL_HEAD: RequestFrameIds = RequestFrameIds { + request_id: 86, + response_id: 87, +}; + +/// Wire discriminants for `chain_unpin_head`. +pub const CHAIN_UNPIN_HEAD: RequestFrameIds = RequestFrameIds { + request_id: 88, + response_id: 89, +}; + +/// Wire discriminants for `chain_continue_head`. +pub const CHAIN_CONTINUE_HEAD: RequestFrameIds = RequestFrameIds { + request_id: 90, + response_id: 91, +}; + +/// Wire discriminants for `chain_stop_head_operation`. +pub const CHAIN_STOP_HEAD_OPERATION: RequestFrameIds = RequestFrameIds { + request_id: 92, + response_id: 93, +}; + +/// Wire discriminants for `chain_get_spec_genesis_hash`. +pub const CHAIN_GET_SPEC_GENESIS_HASH: RequestFrameIds = RequestFrameIds { + request_id: 94, + response_id: 95, +}; + +/// Wire discriminants for `chain_get_spec_chain_name`. +pub const CHAIN_GET_SPEC_CHAIN_NAME: RequestFrameIds = RequestFrameIds { + request_id: 96, + response_id: 97, +}; + +/// Wire discriminants for `chain_get_spec_properties`. +pub const CHAIN_GET_SPEC_PROPERTIES: RequestFrameIds = RequestFrameIds { + request_id: 98, + response_id: 99, +}; + +/// Wire discriminants for `chain_broadcast_transaction`. +pub const CHAIN_BROADCAST_TRANSACTION: RequestFrameIds = RequestFrameIds { + request_id: 100, + response_id: 101, +}; + +/// Wire discriminants for `chain_stop_transaction`. +pub const CHAIN_STOP_TRANSACTION: RequestFrameIds = RequestFrameIds { + request_id: 102, + response_id: 103, +}; + +/// Wire discriminants for `theme_subscribe`. +pub const THEME_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 104, + stop_id: 105, + interrupt_id: 106, + receive_id: 107, +}; + +/// Wire discriminants for `entropy_derive`. +pub const ENTROPY_DERIVE: RequestFrameIds = RequestFrameIds { + request_id: 108, + response_id: 109, +}; + +/// Wire discriminants for `account_get_user_id`. +pub const ACCOUNT_GET_USER_ID: RequestFrameIds = RequestFrameIds { + request_id: 110, + response_id: 111, +}; + +/// Wire discriminants for `account_request_login`. +pub const ACCOUNT_REQUEST_LOGIN: RequestFrameIds = RequestFrameIds { + request_id: 112, + response_id: 113, +}; + +/// Wire discriminants for `signing_sign_raw`. +pub const SIGNING_SIGN_RAW: RequestFrameIds = RequestFrameIds { + request_id: 114, + response_id: 115, +}; + +/// Wire discriminants for `signing_sign_payload`. +pub const SIGNING_SIGN_PAYLOAD: RequestFrameIds = RequestFrameIds { + request_id: 116, + response_id: 117, +}; + +/// Wire discriminants for `payment_balance_subscribe`. +pub const PAYMENT_BALANCE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 118, + stop_id: 119, + interrupt_id: 120, + receive_id: 121, +}; + +/// Wire discriminants for `payment_top_up`. +pub const PAYMENT_TOP_UP: RequestFrameIds = RequestFrameIds { + request_id: 122, + response_id: 123, +}; + +/// Wire discriminants for `payment_request`. +pub const PAYMENT_REQUEST: RequestFrameIds = RequestFrameIds { + request_id: 124, + response_id: 125, +}; + +/// Wire discriminants for `payment_status_subscribe`. +pub const PAYMENT_STATUS_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 126, + stop_id: 127, + interrupt_id: 128, + receive_id: 129, +}; + +/// Wire discriminants for `resource_allocation_request`. +pub const RESOURCE_ALLOCATION_REQUEST: RequestFrameIds = RequestFrameIds { + request_id: 130, + response_id: 131, +}; + +/// Wire discriminants for `statement_store_create_proof_authorized`. +pub const STATEMENT_STORE_CREATE_PROOF_AUTHORIZED: RequestFrameIds = RequestFrameIds { + request_id: 132, + response_id: 133, +}; + +/// Wire discriminants for `notifications_cancel_push_notification`. +pub const NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION: RequestFrameIds = RequestFrameIds { + request_id: 134, + response_id: 135, +}; + +/// Wire discriminants for `coin_payment_create_purse`. +pub const COIN_PAYMENT_CREATE_PURSE: RequestFrameIds = RequestFrameIds { + request_id: 136, + response_id: 137, +}; + +/// Wire discriminants for `coin_payment_query_purse`. +pub const COIN_PAYMENT_QUERY_PURSE: RequestFrameIds = RequestFrameIds { + request_id: 138, + response_id: 139, +}; + +/// Wire discriminants for `coin_payment_rebalance_purse`. +pub const COIN_PAYMENT_REBALANCE_PURSE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 140, + stop_id: 141, + interrupt_id: 142, + receive_id: 143, +}; + +/// Wire discriminants for `coin_payment_delete_purse`. +pub const COIN_PAYMENT_DELETE_PURSE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 144, + stop_id: 145, + interrupt_id: 146, + receive_id: 147, +}; + +/// Wire discriminants for `coin_payment_create_receivable`. +pub const COIN_PAYMENT_CREATE_RECEIVABLE: RequestFrameIds = RequestFrameIds { + request_id: 148, + response_id: 149, +}; + +/// Wire discriminants for `coin_payment_create_cheque`. +pub const COIN_PAYMENT_CREATE_CHEQUE: RequestFrameIds = RequestFrameIds { + request_id: 150, + response_id: 151, +}; + +/// Wire discriminants for `coin_payment_deposit`. +pub const COIN_PAYMENT_DEPOSIT: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 152, + stop_id: 153, + interrupt_id: 154, + receive_id: 155, +}; + +/// Wire discriminants for `coin_payment_refund`. +pub const COIN_PAYMENT_REFUND: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 156, + stop_id: 157, + interrupt_id: 158, + receive_id: 159, +}; + +/// Wire discriminants for `coin_payment_listen_for_payment`. +pub const COIN_PAYMENT_LISTEN_FOR_PAYMENT: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 160, + stop_id: 161, + interrupt_id: 162, + receive_id: 163, +}; + +/// 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(SYSTEM_HANDSHAKE), + }, + WireEntry { + method: "system_feature_supported", + kind: WireKind::Request(SYSTEM_FEATURE_SUPPORTED), + }, + WireEntry { + method: "notifications_send_push_notification", + kind: WireKind::Request(NOTIFICATIONS_SEND_PUSH_NOTIFICATION), + }, + WireEntry { + method: "system_navigate_to", + kind: WireKind::Request(SYSTEM_NAVIGATE_TO), + }, + WireEntry { + method: "permissions_request_device_permission", + kind: WireKind::Request(PERMISSIONS_REQUEST_DEVICE_PERMISSION), + }, + WireEntry { + method: "permissions_request_remote_permission", + kind: WireKind::Request(PERMISSIONS_REQUEST_REMOTE_PERMISSION), + }, + WireEntry { + method: "local_storage_read", + kind: WireKind::Request(LOCAL_STORAGE_READ), + }, + WireEntry { + method: "local_storage_write", + kind: WireKind::Request(LOCAL_STORAGE_WRITE), + }, + WireEntry { + method: "local_storage_clear", + kind: WireKind::Request(LOCAL_STORAGE_CLEAR), + }, + WireEntry { + method: "account_connection_status_subscribe", + kind: WireKind::Subscription(ACCOUNT_CONNECTION_STATUS_SUBSCRIBE), + }, + WireEntry { + method: "account_get_account", + kind: WireKind::Request(ACCOUNT_GET_ACCOUNT), + }, + WireEntry { + method: "account_get_account_alias", + kind: WireKind::Request(ACCOUNT_GET_ACCOUNT_ALIAS), + }, + WireEntry { + method: "account_create_account_proof", + kind: WireKind::Request(ACCOUNT_CREATE_ACCOUNT_PROOF), + }, + WireEntry { + method: "account_get_legacy_accounts", + kind: WireKind::Request(ACCOUNT_GET_LEGACY_ACCOUNTS), + }, + WireEntry { + method: "signing_create_transaction", + kind: WireKind::Request(SIGNING_CREATE_TRANSACTION), + }, + WireEntry { + method: "signing_create_transaction_with_legacy_account", + kind: WireKind::Request(SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT), + }, + WireEntry { + method: "signing_sign_raw_with_legacy_account", + kind: WireKind::Request(SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT), + }, + WireEntry { + method: "signing_sign_payload_with_legacy_account", + kind: WireKind::Request(SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT), + }, + WireEntry { + method: "chat_create_room", + kind: WireKind::Request(CHAT_CREATE_ROOM), + }, + WireEntry { + method: "chat_register_bot", + kind: WireKind::Request(CHAT_REGISTER_BOT), + }, + WireEntry { + method: "chat_list_subscribe", + kind: WireKind::Subscription(CHAT_LIST_SUBSCRIBE), + }, + WireEntry { + method: "chat_post_message", + kind: WireKind::Request(CHAT_POST_MESSAGE), + }, + WireEntry { + method: "chat_action_subscribe", + kind: WireKind::Subscription(CHAT_ACTION_SUBSCRIBE), + }, + WireEntry { + method: "chat_custom_message_render_subscribe", + kind: WireKind::Subscription(CHAT_CUSTOM_MESSAGE_RENDER_SUBSCRIBE), + }, + WireEntry { + method: "statement_store_subscribe", + kind: WireKind::Subscription(STATEMENT_STORE_SUBSCRIBE), + }, + WireEntry { + method: "statement_store_create_proof", + kind: WireKind::Request(STATEMENT_STORE_CREATE_PROOF), + }, + WireEntry { + method: "statement_store_submit", + kind: WireKind::Request(STATEMENT_STORE_SUBMIT), + }, + WireEntry { + method: "preimage_lookup_subscribe", + kind: WireKind::Subscription(PREIMAGE_LOOKUP_SUBSCRIBE), + }, + WireEntry { + method: "preimage_submit", + kind: WireKind::Request(PREIMAGE_SUBMIT), + }, + WireEntry { + method: "chain_follow_head_subscribe", + kind: WireKind::Subscription(CHAIN_FOLLOW_HEAD_SUBSCRIBE), + }, + WireEntry { + method: "chain_get_head_header", + kind: WireKind::Request(CHAIN_GET_HEAD_HEADER), + }, + WireEntry { + method: "chain_get_head_body", + kind: WireKind::Request(CHAIN_GET_HEAD_BODY), + }, + WireEntry { + method: "chain_get_head_storage", + kind: WireKind::Request(CHAIN_GET_HEAD_STORAGE), + }, + WireEntry { + method: "chain_call_head", + kind: WireKind::Request(CHAIN_CALL_HEAD), + }, + WireEntry { + method: "chain_unpin_head", + kind: WireKind::Request(CHAIN_UNPIN_HEAD), + }, + WireEntry { + method: "chain_continue_head", + kind: WireKind::Request(CHAIN_CONTINUE_HEAD), + }, + WireEntry { + method: "chain_stop_head_operation", + kind: WireKind::Request(CHAIN_STOP_HEAD_OPERATION), + }, + WireEntry { + method: "chain_get_spec_genesis_hash", + kind: WireKind::Request(CHAIN_GET_SPEC_GENESIS_HASH), + }, + WireEntry { + method: "chain_get_spec_chain_name", + kind: WireKind::Request(CHAIN_GET_SPEC_CHAIN_NAME), + }, + WireEntry { + method: "chain_get_spec_properties", + kind: WireKind::Request(CHAIN_GET_SPEC_PROPERTIES), + }, + WireEntry { + method: "chain_broadcast_transaction", + kind: WireKind::Request(CHAIN_BROADCAST_TRANSACTION), + }, + WireEntry { + method: "chain_stop_transaction", + kind: WireKind::Request(CHAIN_STOP_TRANSACTION), + }, + WireEntry { + method: "theme_subscribe", + kind: WireKind::Subscription(THEME_SUBSCRIBE), + }, + WireEntry { + method: "entropy_derive", + kind: WireKind::Request(ENTROPY_DERIVE), + }, + WireEntry { + method: "account_get_user_id", + kind: WireKind::Request(ACCOUNT_GET_USER_ID), + }, + WireEntry { + method: "account_request_login", + kind: WireKind::Request(ACCOUNT_REQUEST_LOGIN), + }, + WireEntry { + method: "signing_sign_raw", + kind: WireKind::Request(SIGNING_SIGN_RAW), + }, + WireEntry { + method: "signing_sign_payload", + kind: WireKind::Request(SIGNING_SIGN_PAYLOAD), + }, + WireEntry { + method: "payment_balance_subscribe", + kind: WireKind::Subscription(PAYMENT_BALANCE_SUBSCRIBE), + }, + WireEntry { + method: "payment_top_up", + kind: WireKind::Request(PAYMENT_TOP_UP), + }, + WireEntry { + method: "payment_request", + kind: WireKind::Request(PAYMENT_REQUEST), + }, + WireEntry { + method: "payment_status_subscribe", + kind: WireKind::Subscription(PAYMENT_STATUS_SUBSCRIBE), + }, + WireEntry { + method: "resource_allocation_request", + kind: WireKind::Request(RESOURCE_ALLOCATION_REQUEST), + }, + WireEntry { + method: "statement_store_create_proof_authorized", + kind: WireKind::Request(STATEMENT_STORE_CREATE_PROOF_AUTHORIZED), + }, + WireEntry { + method: "notifications_cancel_push_notification", + kind: WireKind::Request(NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION), + }, + WireEntry { + method: "coin_payment_create_purse", + kind: WireKind::Request(COIN_PAYMENT_CREATE_PURSE), + }, + WireEntry { + method: "coin_payment_query_purse", + kind: WireKind::Request(COIN_PAYMENT_QUERY_PURSE), + }, + WireEntry { + method: "coin_payment_rebalance_purse", + kind: WireKind::Subscription(COIN_PAYMENT_REBALANCE_PURSE), + }, + WireEntry { + method: "coin_payment_delete_purse", + kind: WireKind::Subscription(COIN_PAYMENT_DELETE_PURSE), + }, + WireEntry { + method: "coin_payment_create_receivable", + kind: WireKind::Request(COIN_PAYMENT_CREATE_RECEIVABLE), + }, + WireEntry { + method: "coin_payment_create_cheque", + kind: WireKind::Request(COIN_PAYMENT_CREATE_CHEQUE), + }, + WireEntry { + method: "coin_payment_deposit", + kind: WireKind::Subscription(COIN_PAYMENT_DEPOSIT), + }, + WireEntry { + method: "coin_payment_refund", + kind: WireKind::Subscription(COIN_PAYMENT_REFUND), + }, + WireEntry { + method: "coin_payment_listen_for_payment", + kind: WireKind::Subscription(COIN_PAYMENT_LISTEN_FOR_PAYMENT), + }, +]; 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..d4dd04c2 --- /dev/null +++ b/rust/crates/truapi-codegen/tests/golden_rust_emit.rs @@ -0,0 +1,148 @@ +//! Golden snapshot test for the Rust dispatcher emitter. +//! +//! 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::{Path, PathBuf}; +use std::process::Command; + +/// 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 { + produce_rustdoc_json_for_package(workspace_root, target_dir, "truapi") +} + +fn produce_rustdoc_json_for_package( + workspace_root: &Path, + target_dir: &Path, + package: &str, +) -> PathBuf { + let output = Command::new("cargo") + .args(["+nightly", "rustdoc", "-p", package, "--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 -p {package}` 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_name = package.replace('-', "_"); + let json = target_dir.join(format!("doc/{json_name}.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() +} + +#[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", + 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 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(); + 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-server/Cargo.toml b/rust/crates/truapi-server/Cargo.toml new file mode 100644 index 00000000..138e4212 --- /dev/null +++ b/rust/crates/truapi-server/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "truapi-server" +version = "0.1.0" +edition.workspace = true +license.workspace = true +description = "TrUAPI host runtime: frames, dispatcher, and subscriptions" + +[dependencies] +truapi = { path = "../truapi" } +futures = "0.3" +parity-scale-codec = { version = "3", features = ["derive"] } +tracing = "0.1" diff --git a/rust/crates/truapi-server/src/dispatcher.rs b/rust/crates/truapi-server/src/dispatcher.rs new file mode 100644 index 00000000..20389abd --- /dev/null +++ b/rust/crates/truapi-server/src/dispatcher.rs @@ -0,0 +1,310 @@ +//! Request dispatcher. +//! +//! Routes incoming frames to the appropriate trait method based on the +//! numeric wire discriminant. The handler set is registered by the +//! auto-generated [`crate::generated::dispatcher::register`] function; this +//! module provides the framework that owns the registration tables and the +//! routing logic. + +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; + +use futures::future::LocalBoxFuture; +use tracing::instrument; + +use crate::frame::{Payload, ProtocolMessage}; +use crate::generated::wire_table::{RequestFrameIds, SubscriptionFrameIds}; +use crate::subscription::{Spawner, SubscriptionManager, SubscriptionStream}; +use crate::transport::Transport; + +/// 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. 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. 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>> + + Send + + Sync, +>; + +/// A registered request handler plus the discriminants it replies on. +pub struct RequestEntry { + ids: RequestFrameIds, + handler: RequestHandler, +} + +/// A registered subscription handler plus the discriminants its frames carry. +pub struct SubscriptionEntry { + ids: SubscriptionFrameIds, + handler: SubscriptionHandler, +} + +/// Routes incoming protocol messages to registered handlers, keyed on the +/// numeric wire discriminant. +pub struct Dispatcher { + by_request: HashMap, + by_start: HashMap, + stop_ids: HashSet, + subscriptions: SubscriptionManager, +} + +impl Dispatcher { + /// Construct a dispatcher whose subscriptions are driven on `spawner`. + pub fn new(spawner: Spawner) -> Self { + Self { + by_request: HashMap::new(), + by_start: HashMap::new(), + stop_ids: HashSet::new(), + subscriptions: SubscriptionManager::new(spawner), + } + } + + /// Register a request-response handler, keyed on `ids.request_id`. Returns + /// the previously registered entry if any; callers (the generated + /// `dispatcher::register`) should treat `Some` as a programming error + /// since each request id must own exactly one handler. + pub fn on_request(&mut self, ids: RequestFrameIds, handler: F) -> Option + where + F: Fn(String, Vec) -> LocalBoxFuture<'static, Result, Vec>> + + Send + + Sync + + 'static, + { + self.by_request.insert( + ids.request_id, + RequestEntry { + ids, + handler: Arc::new(handler), + }, + ) + } + + /// Register a subscription handler, keyed on `ids.start_id`, and record + /// `ids.stop_id` so a matching `_stop` frame tears the subscription down. + /// Returns the previously registered entry if any. + pub fn on_subscription( + &mut self, + ids: SubscriptionFrameIds, + handler: F, + ) -> Option + where + F: Fn(String, Vec) -> LocalBoxFuture<'static, Result>> + + Send + + Sync + + 'static, + { + self.stop_ids.insert(ids.stop_id); + self.by_start.insert( + ids.start_id, + SubscriptionEntry { + ids, + handler: Arc::new(handler), + }, + ) + } + + /// Process an incoming protocol message, sending any responses or + /// subscription frames through `transport`. A discriminant with no + /// registered handler is dropped. + #[instrument(skip_all, fields(runtime.method = "dispatcher.dispatch"))] + pub async fn dispatch(&self, message: ProtocolMessage, transport: Arc) { + let id = message.payload.id; + + if let Some(entry) = self.by_request.get(&id) { + // On the wire, every response is `Result`-shaped: the + // handler returns `Ok(bytes)` already prefixed with a `0x00` + // discriminant for success, and `Err(bytes)` whose bytes are the + // SCALE-encoded `CallError`. The error path prepends `0x01` so the + // wire payload is always `[disc][value...]`. + let request_id = message.request_id.clone(); + let value = match (entry.handler)(request_id, message.payload.value).await { + Ok(value) => value, + Err(err_bytes) => prefix_err(err_bytes), + }; + transport.send(ProtocolMessage { + request_id: message.request_id, + payload: Payload { + id: entry.ids.response_id, + value, + }, + }); + } else if let Some(entry) = self.by_start.get(&id) { + // Reserve the slot before awaiting the handler so a `_stop` + // arriving while the handler resolves cancels the pending + // subscription instead of racing the registration. + let token = self.subscriptions.reserve(message.request_id.clone()); + let request_id = message.request_id.clone(); + match (entry.handler)(request_id, message.payload.value).await { + Ok(stream) => { + self.subscriptions.activate( + token, + entry.ids.receive_id, + entry.ids.interrupt_id, + stream, + transport, + ); + } + Err(err_bytes) => { + self.subscriptions.cancel_reservation(token); + transport.send(ProtocolMessage { + request_id: message.request_id, + payload: Payload { + id: entry.ids.interrupt_id, + value: err_bytes, + }, + }); + } + } + } else if self.stop_ids.contains(&id) { + self.subscriptions.handle_stop(&message.request_id); + } + // Unknown discriminant: drop. Response / receive / interrupt frames are + // handled by the client side and never registered here. + } +} + +/// Prepend the `0x01` Err discriminant to SCALE-encoded `CallError` bytes, +/// producing the `[disc][value...]` Result wire shape the response envelope +/// expects. +fn prefix_err(err_bytes: Vec) -> Vec { + let mut value = Vec::with_capacity(1 + err_bytes.len()); + value.push(1u8); + value.extend_from_slice(&err_bytes); + value +} + +#[cfg(test)] +mod tests { + use super::*; + use parity_scale_codec::Encode; + use std::sync::Mutex; + use truapi::CallError; + + fn test_spawner() -> Spawner { + #[cfg(not(target_arch = "wasm32"))] + { + crate::subscription::thread_per_subscription_spawner() + } + #[cfg(target_arch = "wasm32")] + { + Arc::new(futures::executor::block_on) + } + } + + #[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(id: u8, value: Vec) -> ProtocolMessage { + ProtocolMessage { + request_id: "p:1".into(), + payload: Payload { id, value }, + } + } + + /// A frame whose discriminant has no registered handler is dropped: no + /// response, no interrupt. (In production `register` registers every wire + /// method, so this only happens for malformed or client-bound ids.) + #[test] + fn dispatch_unregistered_id_sends_nothing() { + let dispatcher = Dispatcher::new(test_spawner()); + let transport = Arc::new(RecordingTransport::default()); + let transport_dyn: Arc = transport.clone(); + let frame = make_frame(250, Vec::new()); + futures::executor::block_on(dispatcher.dispatch(frame, transport_dyn)); + assert!( + transport.sent().is_empty(), + "an unregistered discriminant must produce no frame" + ); + } + + /// A handler that returns `Err(CallError::Denied)` must produce a response + /// frame on the registered `response_id` 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(test_spawner()); + let ids = RequestFrameIds { + request_id: 200, + response_id: 201, + }; + dispatcher.on_request(ids, |_request_id, _bytes| { + Box::pin(async move { + let err: CallError<()> = CallError::Denied; + Err(crate::frame::encode_call_error_payload(err)) + }) + }); + let transport = Arc::new(RecordingTransport::default()); + let frame = make_frame(200, Vec::new()); + futures::executor::block_on(dispatcher.dispatch(frame, transport.clone())); + let sent = transport.sent(); + assert_eq!(sent.len(), 1, "exactly one response expected"); + assert_eq!(sent[0].payload.id, 201); + 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(test_spawner()); + let ids = RequestFrameIds { + request_id: 200, + response_id: 201, + }; + let prev = dispatcher.on_request(ids, |_request_id, _bytes| { + Box::pin(async move { Ok(Vec::new()) }) + }); + assert!(prev.is_none(), "first registration has no predecessor"); + let prev = dispatcher.on_request(ids, |_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..40d46305 --- /dev/null +++ b/rust/crates/truapi-server/src/frame.rs @@ -0,0 +1,436 @@ +//! 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 slot 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 numeric id directly so dispatch does not need to +//! reconstruct string action tags on every frame. + +use parity_scale_codec::{Decode, Encode, Error as CodecError, Input, Output}; + +use truapi::CallError; + +use crate::generated::wire_table::{RequestFrameIds, SubscriptionFrameIds, WIRE_TABLE, WireKind}; + +/// Top-level wire message. Encoded as `[requestId][discriminant][bytes]`. +#[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, +} + +/// 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) +} + +/// 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) +} + +/// Encode a successful `Result` payload. The leading `0u8` is the SCALE +/// discriminant for `Ok`, followed by the encoded success value. For `()`, +/// this returns just the discriminant because unit encodes to zero bytes. +pub fn encode_ok_payload(value: T) -> Vec { + let mut out = Vec::with_capacity(1 + value.size_hint()); + 0u8.encode_to(&mut out); + value.encode_to(&mut out); + out +} + +impl Encode for ProtocolMessage { + fn encode_to(&self, dest: &mut T) { + self.request_id.encode_to(dest); + self.payload.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 public versioned enum transport 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)?; + // Unknown ids are accepted here; routing is deferred to dispatch, + // which drops frames with no registered handler. + 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 { id, value }, + }) + } +} + +/// Tagged payload. The `id` is the wire discriminant from +/// [`crate::generated::wire_table::WIRE_TABLE`], identifying the frame's method +/// and kind (request/response/start/stop/interrupt/receive). +/// +/// 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 `id`, value bytes already +/// SCALE-encoded by the call site). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Payload { + /// Wire discriminant identifying the frame's method and kind. + pub id: u8, + /// SCALE-encoded inner value bytes. + pub value: Vec, +} + +/// Request discriminants for a request method, by name. Walks the generated +/// [`WIRE_TABLE`]; intended for tests and embedders that route by method +/// string rather than holding the generated const. +pub fn request_ids(method: &str) -> Option { + WIRE_TABLE + .iter() + .find_map(|entry| match (&entry.kind, entry.method == method) { + (WireKind::Request(ids), true) => Some(*ids), + _ => None, + }) +} + +/// Subscription discriminants for a subscription method, by name. Walks the +/// generated [`WIRE_TABLE`]. +pub fn subscription_ids(method: &str) -> Option { + WIRE_TABLE + .iter() + .find_map(|entry| match (&entry.kind, entry.method == method) { + (WireKind::Subscription(ids), true) => Some(*ids), + _ => None, + }) +} + +/// 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) + } +} + +/// 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(id: u8, value: Vec) -> ProtocolMessage { + ProtocolMessage { + request_id: "p:1".to_string(), + payload: Payload { id, value }, + } + } + + fn expected_wire(id: u8, value: &[u8]) -> Vec { + let mut out = Vec::new(); + "p:1".to_string().encode_to(&mut out); + out.push(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(0, 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(22, inner.clone()); + assert_eq!(msg.encode(), expected_wire(22, &inner)); + } + + #[test] + fn round_trip_preserves_id_and_value() { + let inner: Vec = vec![0x00, 0x42, 0xab, 0xcd]; + let msg = build(12, inner.clone()); + let decoded = ProtocolMessage::decode(&mut &msg.encode()[..]).expect("decode"); + assert_eq!(decoded, msg); + } + + /// An unknown discriminant is no longer rejected at decode; routing is + /// deferred to dispatch (which drops frames with no registered handler). + #[test] + fn unknown_discriminant_decodes_ok() { + 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(&[0xaa, 0xbb]); + let decoded = ProtocolMessage::decode(&mut &bytes[..]).expect("unknown id must decode"); + assert_eq!(decoded.payload.id, 250); + assert_eq!(decoded.payload.value, vec![0xaa, 0xbb]); + } + + /// All four subscription phases round-trip through the codec. Catches a + /// regression where `Decode` mishandles a frame whose payload is empty for + /// `_stop` / `_interrupt` (no inner data) but non-empty for `_start` / + /// `_receive`. The ids are the `account_connection_status_subscribe` + /// quartet (18..=21). + #[test] + fn subscription_phases_round_trip_through_codec() { + let cases: &[(u8, Vec)] = &[ + (18, vec![0x00, 0xaa]), // start + (19, Vec::new()), // stop + (20, Vec::new()), // interrupt + (21, vec![0x01, 0x02, 0x03, 0x04]), // receive + ]; + for (id, value) in cases { + let msg = build(*id, value.clone()); + let bytes = msg.encode(); + assert_eq!( + bytes, + expected_wire(*id, value), + "encode mismatch for id {id}" + ); + let decoded = ProtocolMessage::decode(&mut &bytes[..]).expect("decode"); + assert_eq!(decoded, msg, "round-trip mismatch for id {id}"); + } + } + + /// `request_ids` / `subscription_ids` resolve a method name to its + /// generated discriminants without going through the codec. + #[test] + fn id_helpers_resolve_known_methods() { + let handshake = request_ids("system_handshake").expect("known request method"); + assert_eq!(handshake.request_id, 0); + assert_eq!(handshake.response_id, 1); + + let get_account = request_ids("account_get_account").expect("known request method"); + assert_eq!(get_account.request_id, 22); + + let sub = + subscription_ids("account_connection_status_subscribe").expect("known subscription"); + assert_eq!(sub.start_id, 18); + assert_eq!(sub.stop_id, 19); + assert_eq!(sub.interrupt_id, 20); + assert_eq!(sub.receive_id, 21); + + // A request method is not a subscription and vice versa. + assert!(subscription_ids("system_handshake").is_none()); + assert!(request_ids("account_connection_status_subscribe").is_none()); + assert!(request_ids("not_a_method").is_none()); + } + + /// 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() { + // local_storage_clear_response = 17. + let msg = build(17, 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 { + id: 22, + 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 { + id: 22, + 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 { + id: 22, + 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(22, big); + let decoded = ProtocolMessage::decode(&mut &msg.encode()[..]).expect("decode"); + assert_eq!(decoded, msg); + } + + #[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); + } + + #[test] + fn encode_ok_payload_wraps_success_values() { + assert_eq!(encode_ok_payload(()), vec![0u8]); + + let mut expected = vec![0u8]; + 7u32.encode_to(&mut expected); + assert_eq!(encode_ok_payload(7u32), expected); + } + + /// 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/generated/dispatcher.rs b/rust/crates/truapi-server/src/generated/dispatcher.rs new file mode 100644 index 00000000..fc491f50 --- /dev/null +++ b/rust/crates/truapi-server/src/generated/dispatcher.rs @@ -0,0 +1,1468 @@ +//! Wire dispatcher for the unified `TrUApi` trait. +//! +//! Auto-generated by truapi-codegen. Do not edit. + +use std::sync::Arc; + +use parity_scale_codec::Decode; + +use truapi::CallContext; +use truapi::api::{ + Account, Chain, Chat, CoinPayment, Entropy, LocalStorage, Notifications, Payment, Permissions, + Preimage, ResourceAllocation, Signing, StatementStore, System, Theme, +}; +use truapi::versioned; + +use crate::dispatcher::Dispatcher; +use crate::frame::{encode_call_error_payload, encode_decode_error, encode_ok_payload}; +use crate::generated::wire_table; +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 + + CoinPayment + + Entropy + + LocalStorage + + Notifications + + 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_coin_payment(dispatcher, host.clone()); + register_entropy(dispatcher, host.clone()); + register_local_storage(dispatcher, host.clone()); + register_notifications(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( + wire_table::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( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host; + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } +} + +fn register_chain

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Chain + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription( + wire_table::CHAIN_FOLLOW_HEAD_SUBSCRIBE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadFollowRequest = + 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::< + versioned::chain::RemoteChainHeadFollowItem, + _, + >(stream)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host; + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } +} + +fn register_chat

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Chat + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_subscription( + wire_table::CHAT_LIST_SUBSCRIBE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let _ = 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( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_subscription( + wire_table::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( + wire_table::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| 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::< + versioned::chat::ProductChatCustomMessageRenderSubscribeItem, + _, + >(stream)) + }) + }, + ); + } +} + +fn register_coin_payment

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: CoinPayment + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request( + wire_table::COIN_PAYMENT_CREATE_PURSE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentCreatePurseRequest = + 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::coin_payment::HostCoinPaymentCreatePurseResponse = + match host.create_purse(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::COIN_PAYMENT_QUERY_PURSE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentQueryPurseRequest = + 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::coin_payment::HostCoinPaymentQueryPurseResponse = + match host.query_purse(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_subscription( + wire_table::COIN_PAYMENT_REBALANCE_PURSE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentRebalancePurseRequest = + 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.rebalance_purse(&cx, request).await { + Ok(sub) => sub, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(subscription_stream::< + versioned::coin_payment::HostCoinPaymentRebalancePurseItem, + _, + >(stream)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_subscription( + wire_table::COIN_PAYMENT_DELETE_PURSE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentDeletePurseRequest = + 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.delete_purse(&cx, request).await { + Ok(sub) => sub, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(subscription_stream::< + versioned::coin_payment::HostCoinPaymentDeletePurseItem, + _, + >(stream)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::COIN_PAYMENT_CREATE_RECEIVABLE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentCreateReceivableRequest = + 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::coin_payment::HostCoinPaymentCreateReceivableResponse = + match host.create_receivable(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::COIN_PAYMENT_CREATE_CHEQUE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentCreateChequeRequest = + 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::coin_payment::HostCoinPaymentCreateChequeResponse = + match host.create_cheque(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_subscription( + wire_table::COIN_PAYMENT_DEPOSIT, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentDepositRequest = + 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.deposit(&cx, request).await { + Ok(sub) => sub, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(subscription_stream::< + versioned::coin_payment::HostCoinPaymentDepositItem, + _, + >(stream)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_subscription( + wire_table::COIN_PAYMENT_REFUND, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentRefundRequest = + 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.refund(&cx, request).await { + Ok(sub) => sub, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(subscription_stream::< + versioned::coin_payment::HostCoinPaymentRefundItem, + _, + >(stream)) + }) + }, + ); + } + { + let host = host; + dispatcher.on_subscription( + wire_table::COIN_PAYMENT_LISTEN_FOR_PAYMENT, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentListenForRequest = + 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.listen_for_payment(&cx, request).await { + Ok(sub) => sub, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(subscription_stream::< + versioned::coin_payment::HostCoinPaymentListenForItem, + _, + >(stream)) + }) + }, + ); + } +} + +fn register_entropy

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Entropy + Send + Sync + 'static, +{ + { + let host = host; + dispatcher.on_request( + wire_table::ENTROPY_DERIVE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::entropy::HostDeriveEntropyRequest = + 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } +} + +fn register_local_storage

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: LocalStorage + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request( + wire_table::LOCAL_STORAGE_READ, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::local_storage::HostLocalStorageReadRequest = + 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host; + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } +} + +fn register_notifications

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Notifications + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request( + wire_table::NOTIFICATIONS_SEND_PUSH_NOTIFICATION, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::notifications::HostPushNotificationRequest = + 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::notifications::HostPushNotificationResponse = + match host.send_push_notification(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host; + dispatcher.on_request( + wire_table::NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::notifications::HostPushNotificationCancelRequest = + 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::notifications::HostPushNotificationCancelResponse = + match host.cancel_push_notification(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } +} + +fn register_payment

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Payment + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription( + wire_table::PAYMENT_BALANCE_SUBSCRIBE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::payment::HostPaymentBalanceSubscribeRequest = + 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(encode_call_error_payload(err)), + }; + Ok(subscription_stream::< + versioned::payment::HostPaymentBalanceSubscribeItem, + _, + >(stream)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::PAYMENT_REQUEST, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::payment::HostPaymentRequest = + 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::HostPaymentResponse = + match host.request(&cx, request).await { + Ok(value) => value, + Err(err) => return Err(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_subscription( + wire_table::PAYMENT_STATUS_SUBSCRIBE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::payment::HostPaymentStatusSubscribeRequest = + 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(encode_call_error_payload(err)), + }; + Ok(subscription_stream::< + versioned::payment::HostPaymentStatusSubscribeItem, + _, + >(stream)) + }) + }, + ); + } + { + let host = host; + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } +} + +fn register_permissions

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Permissions + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request( + wire_table::PERMISSIONS_REQUEST_DEVICE_PERMISSION, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::permissions::HostDevicePermissionRequest = + 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host; + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } +} + +fn register_preimage

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Preimage + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription( + wire_table::PREIMAGE_LOOKUP_SUBSCRIBE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::preimage::RemotePreimageLookupSubscribeRequest = + 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::< + versioned::preimage::RemotePreimageLookupSubscribeItem, + _, + >(stream)) + }) + }, + ); + } + { + let host = host; + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } +} + +fn register_resource_allocation

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: ResourceAllocation + Send + Sync + 'static, +{ + { + let host = host; + dispatcher.on_request(wire_table::RESOURCE_ALLOCATION_REQUEST, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::resource_allocation::HostRequestResourceAllocationRequest = + 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } +} + +fn register_signing

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Signing + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request( + wire_table::SIGNING_CREATE_TRANSACTION, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostCreateTransactionRequest = + 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host; + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } +} + +fn register_statement_store

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: StatementStore + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription( + wire_table::STATEMENT_STORE_SUBSCRIBE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::statement_store::RemoteStatementStoreSubscribeRequest = + 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::< + versioned::statement_store::RemoteStatementStoreSubscribeItem, + _, + >(stream)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request( + wire_table::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| encode_decode_error(e.to_string()))?; + let cx = CallContext::with_request_id(request_id.clone()); + match host.submit(&cx, request).await { + Ok(()) => Ok(encode_ok_payload(())), + Err(err) => Err(encode_call_error_payload(err)), + } + }) + }, + ); + } +} + +fn register_system

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: System + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request( + wire_table::SYSTEM_HANDSHAKE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::system::HostHandshakeRequest = + 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } + { + let host = host; + dispatcher.on_request( + wire_table::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| 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(encode_call_error_payload(err)), + }; + Ok(encode_ok_payload(response)) + }) + }, + ); + } +} + +fn register_theme

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Theme + Send + Sync + 'static, +{ + { + let host = host; + dispatcher.on_subscription( + wire_table::THEME_SUBSCRIBE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let _ = 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..12a72a94 --- /dev/null +++ b/rust/crates/truapi-server/src/generated/wire_table.rs @@ -0,0 +1,722 @@ +//! 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 ids for each method are exposed +//! as a named const (`PREIMAGE_SUBMIT`, ...); [`WIRE_TABLE`] and the +//! generated dispatcher both reference those consts so the numbers live +//! in exactly one place. The table is sorted by request/start id. + +/// Request method wire discriminants. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RequestFrameIds { + /// Discriminant for the request frame. + pub request_id: u8, + /// Discriminant for the response frame. + pub response_id: u8, +} + +/// Subscription method wire discriminants. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SubscriptionFrameIds { + /// Discriminant for the start frame. + pub start_id: u8, + /// Discriminant for the stop frame. + pub stop_id: u8, + /// Discriminant for the interrupt frame (server-initiated termination). + pub interrupt_id: u8, + /// Discriminant for each receive frame (a streamed item). + pub receive_id: u8, +} + +/// A single wire-table row. +pub struct WireEntry { + /// Method name from the Rust trait. + pub method: &'static str, + /// What kind of slot this entry describes. + pub kind: WireKind, +} + +/// Wire-slot shape: request/response pair or subscription quartet. +pub enum WireKind { + /// Request/response method. + Request(RequestFrameIds), + /// Subscription method. + Subscription(SubscriptionFrameIds), +} + +/// Wire discriminants for `system_handshake`. +pub const SYSTEM_HANDSHAKE: RequestFrameIds = RequestFrameIds { + request_id: 0, + response_id: 1, +}; + +/// Wire discriminants for `system_feature_supported`. +pub const SYSTEM_FEATURE_SUPPORTED: RequestFrameIds = RequestFrameIds { + request_id: 2, + response_id: 3, +}; + +/// Wire discriminants for `notifications_send_push_notification`. +pub const NOTIFICATIONS_SEND_PUSH_NOTIFICATION: RequestFrameIds = RequestFrameIds { + request_id: 4, + response_id: 5, +}; + +/// Wire discriminants for `system_navigate_to`. +pub const SYSTEM_NAVIGATE_TO: RequestFrameIds = RequestFrameIds { + request_id: 6, + response_id: 7, +}; + +/// Wire discriminants for `permissions_request_device_permission`. +pub const PERMISSIONS_REQUEST_DEVICE_PERMISSION: RequestFrameIds = RequestFrameIds { + request_id: 8, + response_id: 9, +}; + +/// Wire discriminants for `permissions_request_remote_permission`. +pub const PERMISSIONS_REQUEST_REMOTE_PERMISSION: RequestFrameIds = RequestFrameIds { + request_id: 10, + response_id: 11, +}; + +/// Wire discriminants for `local_storage_read`. +pub const LOCAL_STORAGE_READ: RequestFrameIds = RequestFrameIds { + request_id: 12, + response_id: 13, +}; + +/// Wire discriminants for `local_storage_write`. +pub const LOCAL_STORAGE_WRITE: RequestFrameIds = RequestFrameIds { + request_id: 14, + response_id: 15, +}; + +/// Wire discriminants for `local_storage_clear`. +pub const LOCAL_STORAGE_CLEAR: RequestFrameIds = RequestFrameIds { + request_id: 16, + response_id: 17, +}; + +/// Wire discriminants for `account_connection_status_subscribe`. +pub const ACCOUNT_CONNECTION_STATUS_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 18, + stop_id: 19, + interrupt_id: 20, + receive_id: 21, +}; + +/// Wire discriminants for `account_get_account`. +pub const ACCOUNT_GET_ACCOUNT: RequestFrameIds = RequestFrameIds { + request_id: 22, + response_id: 23, +}; + +/// Wire discriminants for `account_get_account_alias`. +pub const ACCOUNT_GET_ACCOUNT_ALIAS: RequestFrameIds = RequestFrameIds { + request_id: 24, + response_id: 25, +}; + +/// Wire discriminants for `account_create_account_proof`. +pub const ACCOUNT_CREATE_ACCOUNT_PROOF: RequestFrameIds = RequestFrameIds { + request_id: 26, + response_id: 27, +}; + +/// Wire discriminants for `account_get_legacy_accounts`. +pub const ACCOUNT_GET_LEGACY_ACCOUNTS: RequestFrameIds = RequestFrameIds { + request_id: 28, + response_id: 29, +}; + +/// Wire discriminants for `signing_create_transaction`. +pub const SIGNING_CREATE_TRANSACTION: RequestFrameIds = RequestFrameIds { + request_id: 30, + response_id: 31, +}; + +/// Wire discriminants for `signing_create_transaction_with_legacy_account`. +pub const SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { + request_id: 32, + response_id: 33, +}; + +/// Wire discriminants for `signing_sign_raw_with_legacy_account`. +pub const SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { + request_id: 34, + response_id: 35, +}; + +/// Wire discriminants for `signing_sign_payload_with_legacy_account`. +pub const SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { + request_id: 36, + response_id: 37, +}; + +/// Wire discriminants for `chat_create_room`. +pub const CHAT_CREATE_ROOM: RequestFrameIds = RequestFrameIds { + request_id: 38, + response_id: 39, +}; + +/// Wire discriminants for `chat_register_bot`. +pub const CHAT_REGISTER_BOT: RequestFrameIds = RequestFrameIds { + request_id: 40, + response_id: 41, +}; + +/// Wire discriminants for `chat_list_subscribe`. +pub const CHAT_LIST_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 42, + stop_id: 43, + interrupt_id: 44, + receive_id: 45, +}; + +/// Wire discriminants for `chat_post_message`. +pub const CHAT_POST_MESSAGE: RequestFrameIds = RequestFrameIds { + request_id: 46, + response_id: 47, +}; + +/// Wire discriminants for `chat_action_subscribe`. +pub const CHAT_ACTION_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 48, + stop_id: 49, + interrupt_id: 50, + receive_id: 51, +}; + +/// Wire discriminants for `chat_custom_message_render_subscribe`. +pub const CHAT_CUSTOM_MESSAGE_RENDER_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 52, + stop_id: 53, + interrupt_id: 54, + receive_id: 55, +}; + +/// Wire discriminants for `statement_store_subscribe`. +pub const STATEMENT_STORE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 56, + stop_id: 57, + interrupt_id: 58, + receive_id: 59, +}; + +/// Wire discriminants for `statement_store_create_proof`. +pub const STATEMENT_STORE_CREATE_PROOF: RequestFrameIds = RequestFrameIds { + request_id: 60, + response_id: 61, +}; + +/// Wire discriminants for `statement_store_submit`. +pub const STATEMENT_STORE_SUBMIT: RequestFrameIds = RequestFrameIds { + request_id: 62, + response_id: 63, +}; + +/// Wire discriminants for `preimage_lookup_subscribe`. +pub const PREIMAGE_LOOKUP_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 64, + stop_id: 65, + interrupt_id: 66, + receive_id: 67, +}; + +/// Wire discriminants for `preimage_submit`. +pub const PREIMAGE_SUBMIT: RequestFrameIds = RequestFrameIds { + request_id: 68, + response_id: 69, +}; + +/// Wire discriminants for `chain_follow_head_subscribe`. +pub const CHAIN_FOLLOW_HEAD_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 76, + stop_id: 77, + interrupt_id: 78, + receive_id: 79, +}; + +/// Wire discriminants for `chain_get_head_header`. +pub const CHAIN_GET_HEAD_HEADER: RequestFrameIds = RequestFrameIds { + request_id: 80, + response_id: 81, +}; + +/// Wire discriminants for `chain_get_head_body`. +pub const CHAIN_GET_HEAD_BODY: RequestFrameIds = RequestFrameIds { + request_id: 82, + response_id: 83, +}; + +/// Wire discriminants for `chain_get_head_storage`. +pub const CHAIN_GET_HEAD_STORAGE: RequestFrameIds = RequestFrameIds { + request_id: 84, + response_id: 85, +}; + +/// Wire discriminants for `chain_call_head`. +pub const CHAIN_CALL_HEAD: RequestFrameIds = RequestFrameIds { + request_id: 86, + response_id: 87, +}; + +/// Wire discriminants for `chain_unpin_head`. +pub const CHAIN_UNPIN_HEAD: RequestFrameIds = RequestFrameIds { + request_id: 88, + response_id: 89, +}; + +/// Wire discriminants for `chain_continue_head`. +pub const CHAIN_CONTINUE_HEAD: RequestFrameIds = RequestFrameIds { + request_id: 90, + response_id: 91, +}; + +/// Wire discriminants for `chain_stop_head_operation`. +pub const CHAIN_STOP_HEAD_OPERATION: RequestFrameIds = RequestFrameIds { + request_id: 92, + response_id: 93, +}; + +/// Wire discriminants for `chain_get_spec_genesis_hash`. +pub const CHAIN_GET_SPEC_GENESIS_HASH: RequestFrameIds = RequestFrameIds { + request_id: 94, + response_id: 95, +}; + +/// Wire discriminants for `chain_get_spec_chain_name`. +pub const CHAIN_GET_SPEC_CHAIN_NAME: RequestFrameIds = RequestFrameIds { + request_id: 96, + response_id: 97, +}; + +/// Wire discriminants for `chain_get_spec_properties`. +pub const CHAIN_GET_SPEC_PROPERTIES: RequestFrameIds = RequestFrameIds { + request_id: 98, + response_id: 99, +}; + +/// Wire discriminants for `chain_broadcast_transaction`. +pub const CHAIN_BROADCAST_TRANSACTION: RequestFrameIds = RequestFrameIds { + request_id: 100, + response_id: 101, +}; + +/// Wire discriminants for `chain_stop_transaction`. +pub const CHAIN_STOP_TRANSACTION: RequestFrameIds = RequestFrameIds { + request_id: 102, + response_id: 103, +}; + +/// Wire discriminants for `theme_subscribe`. +pub const THEME_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 104, + stop_id: 105, + interrupt_id: 106, + receive_id: 107, +}; + +/// Wire discriminants for `entropy_derive`. +pub const ENTROPY_DERIVE: RequestFrameIds = RequestFrameIds { + request_id: 108, + response_id: 109, +}; + +/// Wire discriminants for `account_get_user_id`. +pub const ACCOUNT_GET_USER_ID: RequestFrameIds = RequestFrameIds { + request_id: 110, + response_id: 111, +}; + +/// Wire discriminants for `account_request_login`. +pub const ACCOUNT_REQUEST_LOGIN: RequestFrameIds = RequestFrameIds { + request_id: 112, + response_id: 113, +}; + +/// Wire discriminants for `signing_sign_raw`. +pub const SIGNING_SIGN_RAW: RequestFrameIds = RequestFrameIds { + request_id: 114, + response_id: 115, +}; + +/// Wire discriminants for `signing_sign_payload`. +pub const SIGNING_SIGN_PAYLOAD: RequestFrameIds = RequestFrameIds { + request_id: 116, + response_id: 117, +}; + +/// Wire discriminants for `payment_balance_subscribe`. +pub const PAYMENT_BALANCE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 118, + stop_id: 119, + interrupt_id: 120, + receive_id: 121, +}; + +/// Wire discriminants for `payment_top_up`. +pub const PAYMENT_TOP_UP: RequestFrameIds = RequestFrameIds { + request_id: 122, + response_id: 123, +}; + +/// Wire discriminants for `payment_request`. +pub const PAYMENT_REQUEST: RequestFrameIds = RequestFrameIds { + request_id: 124, + response_id: 125, +}; + +/// Wire discriminants for `payment_status_subscribe`. +pub const PAYMENT_STATUS_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 126, + stop_id: 127, + interrupt_id: 128, + receive_id: 129, +}; + +/// Wire discriminants for `resource_allocation_request`. +pub const RESOURCE_ALLOCATION_REQUEST: RequestFrameIds = RequestFrameIds { + request_id: 130, + response_id: 131, +}; + +/// Wire discriminants for `statement_store_create_proof_authorized`. +pub const STATEMENT_STORE_CREATE_PROOF_AUTHORIZED: RequestFrameIds = RequestFrameIds { + request_id: 132, + response_id: 133, +}; + +/// Wire discriminants for `notifications_cancel_push_notification`. +pub const NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION: RequestFrameIds = RequestFrameIds { + request_id: 134, + response_id: 135, +}; + +/// Wire discriminants for `coin_payment_create_purse`. +pub const COIN_PAYMENT_CREATE_PURSE: RequestFrameIds = RequestFrameIds { + request_id: 136, + response_id: 137, +}; + +/// Wire discriminants for `coin_payment_query_purse`. +pub const COIN_PAYMENT_QUERY_PURSE: RequestFrameIds = RequestFrameIds { + request_id: 138, + response_id: 139, +}; + +/// Wire discriminants for `coin_payment_rebalance_purse`. +pub const COIN_PAYMENT_REBALANCE_PURSE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 140, + stop_id: 141, + interrupt_id: 142, + receive_id: 143, +}; + +/// Wire discriminants for `coin_payment_delete_purse`. +pub const COIN_PAYMENT_DELETE_PURSE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 144, + stop_id: 145, + interrupt_id: 146, + receive_id: 147, +}; + +/// Wire discriminants for `coin_payment_create_receivable`. +pub const COIN_PAYMENT_CREATE_RECEIVABLE: RequestFrameIds = RequestFrameIds { + request_id: 148, + response_id: 149, +}; + +/// Wire discriminants for `coin_payment_create_cheque`. +pub const COIN_PAYMENT_CREATE_CHEQUE: RequestFrameIds = RequestFrameIds { + request_id: 150, + response_id: 151, +}; + +/// Wire discriminants for `coin_payment_deposit`. +pub const COIN_PAYMENT_DEPOSIT: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 152, + stop_id: 153, + interrupt_id: 154, + receive_id: 155, +}; + +/// Wire discriminants for `coin_payment_refund`. +pub const COIN_PAYMENT_REFUND: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 156, + stop_id: 157, + interrupt_id: 158, + receive_id: 159, +}; + +/// Wire discriminants for `coin_payment_listen_for_payment`. +pub const COIN_PAYMENT_LISTEN_FOR_PAYMENT: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 160, + stop_id: 161, + interrupt_id: 162, + receive_id: 163, +}; + +/// 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(SYSTEM_HANDSHAKE), + }, + WireEntry { + method: "system_feature_supported", + kind: WireKind::Request(SYSTEM_FEATURE_SUPPORTED), + }, + WireEntry { + method: "notifications_send_push_notification", + kind: WireKind::Request(NOTIFICATIONS_SEND_PUSH_NOTIFICATION), + }, + WireEntry { + method: "system_navigate_to", + kind: WireKind::Request(SYSTEM_NAVIGATE_TO), + }, + WireEntry { + method: "permissions_request_device_permission", + kind: WireKind::Request(PERMISSIONS_REQUEST_DEVICE_PERMISSION), + }, + WireEntry { + method: "permissions_request_remote_permission", + kind: WireKind::Request(PERMISSIONS_REQUEST_REMOTE_PERMISSION), + }, + WireEntry { + method: "local_storage_read", + kind: WireKind::Request(LOCAL_STORAGE_READ), + }, + WireEntry { + method: "local_storage_write", + kind: WireKind::Request(LOCAL_STORAGE_WRITE), + }, + WireEntry { + method: "local_storage_clear", + kind: WireKind::Request(LOCAL_STORAGE_CLEAR), + }, + WireEntry { + method: "account_connection_status_subscribe", + kind: WireKind::Subscription(ACCOUNT_CONNECTION_STATUS_SUBSCRIBE), + }, + WireEntry { + method: "account_get_account", + kind: WireKind::Request(ACCOUNT_GET_ACCOUNT), + }, + WireEntry { + method: "account_get_account_alias", + kind: WireKind::Request(ACCOUNT_GET_ACCOUNT_ALIAS), + }, + WireEntry { + method: "account_create_account_proof", + kind: WireKind::Request(ACCOUNT_CREATE_ACCOUNT_PROOF), + }, + WireEntry { + method: "account_get_legacy_accounts", + kind: WireKind::Request(ACCOUNT_GET_LEGACY_ACCOUNTS), + }, + WireEntry { + method: "signing_create_transaction", + kind: WireKind::Request(SIGNING_CREATE_TRANSACTION), + }, + WireEntry { + method: "signing_create_transaction_with_legacy_account", + kind: WireKind::Request(SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT), + }, + WireEntry { + method: "signing_sign_raw_with_legacy_account", + kind: WireKind::Request(SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT), + }, + WireEntry { + method: "signing_sign_payload_with_legacy_account", + kind: WireKind::Request(SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT), + }, + WireEntry { + method: "chat_create_room", + kind: WireKind::Request(CHAT_CREATE_ROOM), + }, + WireEntry { + method: "chat_register_bot", + kind: WireKind::Request(CHAT_REGISTER_BOT), + }, + WireEntry { + method: "chat_list_subscribe", + kind: WireKind::Subscription(CHAT_LIST_SUBSCRIBE), + }, + WireEntry { + method: "chat_post_message", + kind: WireKind::Request(CHAT_POST_MESSAGE), + }, + WireEntry { + method: "chat_action_subscribe", + kind: WireKind::Subscription(CHAT_ACTION_SUBSCRIBE), + }, + WireEntry { + method: "chat_custom_message_render_subscribe", + kind: WireKind::Subscription(CHAT_CUSTOM_MESSAGE_RENDER_SUBSCRIBE), + }, + WireEntry { + method: "statement_store_subscribe", + kind: WireKind::Subscription(STATEMENT_STORE_SUBSCRIBE), + }, + WireEntry { + method: "statement_store_create_proof", + kind: WireKind::Request(STATEMENT_STORE_CREATE_PROOF), + }, + WireEntry { + method: "statement_store_submit", + kind: WireKind::Request(STATEMENT_STORE_SUBMIT), + }, + WireEntry { + method: "preimage_lookup_subscribe", + kind: WireKind::Subscription(PREIMAGE_LOOKUP_SUBSCRIBE), + }, + WireEntry { + method: "preimage_submit", + kind: WireKind::Request(PREIMAGE_SUBMIT), + }, + WireEntry { + method: "chain_follow_head_subscribe", + kind: WireKind::Subscription(CHAIN_FOLLOW_HEAD_SUBSCRIBE), + }, + WireEntry { + method: "chain_get_head_header", + kind: WireKind::Request(CHAIN_GET_HEAD_HEADER), + }, + WireEntry { + method: "chain_get_head_body", + kind: WireKind::Request(CHAIN_GET_HEAD_BODY), + }, + WireEntry { + method: "chain_get_head_storage", + kind: WireKind::Request(CHAIN_GET_HEAD_STORAGE), + }, + WireEntry { + method: "chain_call_head", + kind: WireKind::Request(CHAIN_CALL_HEAD), + }, + WireEntry { + method: "chain_unpin_head", + kind: WireKind::Request(CHAIN_UNPIN_HEAD), + }, + WireEntry { + method: "chain_continue_head", + kind: WireKind::Request(CHAIN_CONTINUE_HEAD), + }, + WireEntry { + method: "chain_stop_head_operation", + kind: WireKind::Request(CHAIN_STOP_HEAD_OPERATION), + }, + WireEntry { + method: "chain_get_spec_genesis_hash", + kind: WireKind::Request(CHAIN_GET_SPEC_GENESIS_HASH), + }, + WireEntry { + method: "chain_get_spec_chain_name", + kind: WireKind::Request(CHAIN_GET_SPEC_CHAIN_NAME), + }, + WireEntry { + method: "chain_get_spec_properties", + kind: WireKind::Request(CHAIN_GET_SPEC_PROPERTIES), + }, + WireEntry { + method: "chain_broadcast_transaction", + kind: WireKind::Request(CHAIN_BROADCAST_TRANSACTION), + }, + WireEntry { + method: "chain_stop_transaction", + kind: WireKind::Request(CHAIN_STOP_TRANSACTION), + }, + WireEntry { + method: "theme_subscribe", + kind: WireKind::Subscription(THEME_SUBSCRIBE), + }, + WireEntry { + method: "entropy_derive", + kind: WireKind::Request(ENTROPY_DERIVE), + }, + WireEntry { + method: "account_get_user_id", + kind: WireKind::Request(ACCOUNT_GET_USER_ID), + }, + WireEntry { + method: "account_request_login", + kind: WireKind::Request(ACCOUNT_REQUEST_LOGIN), + }, + WireEntry { + method: "signing_sign_raw", + kind: WireKind::Request(SIGNING_SIGN_RAW), + }, + WireEntry { + method: "signing_sign_payload", + kind: WireKind::Request(SIGNING_SIGN_PAYLOAD), + }, + WireEntry { + method: "payment_balance_subscribe", + kind: WireKind::Subscription(PAYMENT_BALANCE_SUBSCRIBE), + }, + WireEntry { + method: "payment_top_up", + kind: WireKind::Request(PAYMENT_TOP_UP), + }, + WireEntry { + method: "payment_request", + kind: WireKind::Request(PAYMENT_REQUEST), + }, + WireEntry { + method: "payment_status_subscribe", + kind: WireKind::Subscription(PAYMENT_STATUS_SUBSCRIBE), + }, + WireEntry { + method: "resource_allocation_request", + kind: WireKind::Request(RESOURCE_ALLOCATION_REQUEST), + }, + WireEntry { + method: "statement_store_create_proof_authorized", + kind: WireKind::Request(STATEMENT_STORE_CREATE_PROOF_AUTHORIZED), + }, + WireEntry { + method: "notifications_cancel_push_notification", + kind: WireKind::Request(NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION), + }, + WireEntry { + method: "coin_payment_create_purse", + kind: WireKind::Request(COIN_PAYMENT_CREATE_PURSE), + }, + WireEntry { + method: "coin_payment_query_purse", + kind: WireKind::Request(COIN_PAYMENT_QUERY_PURSE), + }, + WireEntry { + method: "coin_payment_rebalance_purse", + kind: WireKind::Subscription(COIN_PAYMENT_REBALANCE_PURSE), + }, + WireEntry { + method: "coin_payment_delete_purse", + kind: WireKind::Subscription(COIN_PAYMENT_DELETE_PURSE), + }, + WireEntry { + method: "coin_payment_create_receivable", + kind: WireKind::Request(COIN_PAYMENT_CREATE_RECEIVABLE), + }, + WireEntry { + method: "coin_payment_create_cheque", + kind: WireKind::Request(COIN_PAYMENT_CREATE_CHEQUE), + }, + WireEntry { + method: "coin_payment_deposit", + kind: WireKind::Subscription(COIN_PAYMENT_DEPOSIT), + }, + WireEntry { + method: "coin_payment_refund", + kind: WireKind::Subscription(COIN_PAYMENT_REFUND), + }, + WireEntry { + method: "coin_payment_listen_for_payment", + kind: WireKind::Subscription(COIN_PAYMENT_LISTEN_FOR_PAYMENT), + }, +]; diff --git a/rust/crates/truapi-server/src/lib.rs b/rust/crates/truapi-server/src/lib.rs new file mode 100644 index 00000000..1e41f5ce --- /dev/null +++ b/rust/crates/truapi-server/src/lib.rs @@ -0,0 +1,18 @@ +//! TrUAPI host runtime: frame codec, dispatcher, and subscription lifecycle. +//! +//! This crate turns an implementation of the `truapi::api` traits into a +//! transport-agnostic host runtime. It does not implement host capabilities +//! such as wallet access, chain access, payment, permissions, or storage. + +#![forbid(unsafe_code)] + +pub mod dispatcher; +pub mod frame; +pub mod generated; +pub mod subscription; +pub mod transport; + +pub use dispatcher::*; +pub use frame::*; +pub use subscription::*; +pub use transport::*; diff --git a/rust/crates/truapi-server/src/subscription.rs b/rust/crates/truapi-server/src/subscription.rs new file mode 100644 index 00000000..8b24fd98 --- /dev/null +++ b/rust/crates/truapi-server/src/subscription.rs @@ -0,0 +1,495 @@ +//! Subscription lifecycle management. +//! +//! Tracks active subscriptions (start/receive/stop/interrupt) and handles +//! 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::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; + +use futures::StreamExt; +use futures::future::{BoxFuture, Either, select}; +use futures::stream::BoxStream; +use parity_scale_codec::Encode; + +use crate::frame::{Payload, ProtocolMessage}; +use crate::transport::Transport; + +type StopFn = Box; + +/// 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. +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()))) +} + +/// Generation-stamped slot tracking the lifecycle of one subscription id. +/// `request_id` is client-controlled and may be reused or raced against a +/// `_stop`, so each reservation carries a monotonic generation and only the +/// owner of the current generation may transition or remove the slot. +enum Slot { + /// Reserved by the dispatcher before its `_start` handler resolved. + /// `cancelled` flips to `true` if a `_stop` arrives in that window so + /// activation aborts instead of leaking an unstoppable stream. + Pending { generation: u64, cancelled: bool }, + /// A live subscription with its cancellation handle. + Live { generation: u64, cancel: StopFn }, +} + +/// Handle returned by [`SubscriptionManager::reserve`] and presented back to +/// [`SubscriptionManager::activate`]. Ties an activation to the exact +/// reservation it belongs to so a superseding `_start` for the same id +/// cannot be activated by a stale handler. +pub struct ReservationToken { + request_id: String, + generation: u64, +} + +/// Manages active subscriptions on the server side. +pub struct SubscriptionManager { + active: Arc>>, + next_generation: Arc, + spawner: Spawner, +} + +impl SubscriptionManager { + /// Create an empty manager driven by `spawner`. + pub fn new(spawner: Spawner) -> Self { + Self { + active: Arc::new(Mutex::new(HashMap::new())), + next_generation: Arc::new(AtomicU64::new(0)), + spawner, + } + } + + /// Reserve the slot for `request_id` before its subscription stream is + /// available. Any live subscription already under that id is stopped and + /// replaced (re-subscribe semantics). A `_stop` arriving before + /// [`activate`](Self::activate) flips the reservation to cancelled. + pub fn reserve(&self, request_id: String) -> ReservationToken { + let generation = self.next_generation.fetch_add(1, Ordering::Relaxed); + let mut active = self.active.lock().unwrap(); + if let Some(Slot::Live { cancel, .. }) = active.insert( + request_id.clone(), + Slot::Pending { + generation, + cancelled: false, + }, + ) { + cancel(); + } + ReservationToken { + request_id, + generation, + } + } + + /// Drop a reservation whose `_start` handler failed before producing a + /// stream. No-op if the slot was superseded by a newer reservation. + pub fn cancel_reservation(&self, token: ReservationToken) { + let mut active = self.active.lock().unwrap(); + let owned = matches!( + active.get(&token.request_id), + Some(Slot::Pending { generation, .. }) if *generation == token.generation + ); + if owned { + active.remove(&token.request_id); + } + } + + /// Activate a reserved subscription with its stream, forwarding stream + /// items as `_receive` frames until the stream ends or `_stop` is + /// received. No-ops without starting the stream if the reservation was + /// cancelled by a `_stop` or superseded by a newer reservation for the + /// same id. + pub fn activate( + &self, + token: ReservationToken, + receive_id: u8, + interrupt_id: u8, + mut stream: SubscriptionStream, + transport: Arc, + ) { + let ReservationToken { + request_id, + generation, + } = token; + let rid = request_id.clone(); + let stream_transport = transport.clone(); + + // Cancellation channel. + let (cancel_tx, cancel_rx) = futures::channel::oneshot::channel::<()>(); + + // Transition the reserved slot to live, unless a `_stop` cancelled it + // or a newer reservation superseded it while the handler resolved. + { + let mut active = self.active.lock().unwrap(); + match active.get(&request_id) { + Some(Slot::Pending { + generation: g, + cancelled, + }) if *g == generation => { + if *cancelled { + active.remove(&request_id); + return; + } + } + _ => return, + } + active.insert( + request_id.clone(), + Slot::Live { + generation, + cancel: Box::new(move || { + let _ = cancel_tx.send(()); + }), + }, + ); + } + + let active = self.active.clone(); + + let future: BoxFuture<'static, ()> = Box::pin(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 { + id: receive_id, + value, + }, + }) + } + Some(SubscriptionOutput::Interrupt(value)) => { + stream_transport.send(ProtocolMessage { + request_id: rid.clone(), + payload: Payload { + id: interrupt_id, + value, + }, + }); + break false; + } + None => break true, + } + } + } + } + }; + + // Only remove the slot if it still holds THIS generation; a + // superseding reservation owns its own cleanup. + let removed = { + let mut active = active.lock().unwrap(); + let owned = matches!( + active.get(&request_id), + Some(Slot::Live { generation: g, .. }) if *g == generation + ); + if owned { + active.remove(&request_id); + } + owned + }; + + if completed && removed { + transport.send(ProtocolMessage { + request_id, + payload: Payload { + id: interrupt_id, + value: Vec::new(), + }, + }); + } + }); + + (self.spawner)(future); + } + + /// Convenience for callers that already hold the stream with no async gap + /// between reservation and activation (tests and synchronous embedders). + pub fn register( + &self, + request_id: String, + receive_id: u8, + interrupt_id: u8, + stream: SubscriptionStream, + transport: Arc, + ) { + let token = self.reserve(request_id); + self.activate(token, receive_id, interrupt_id, stream, transport); + } + + /// Handle a `_stop` frame from the product side. Cancels a live + /// subscription, or marks a still-pending reservation cancelled so its + /// in-flight activation aborts rather than leaking an unstoppable stream. + pub fn handle_stop(&self, request_id: &str) { + let mut active = self.active.lock().unwrap(); + match active.get_mut(request_id) { + Some(Slot::Pending { cancelled, .. }) => { + *cancelled = true; + } + Some(Slot::Live { .. }) => { + if let Some(Slot::Live { cancel, .. }) = active.remove(request_id) { + cancel(); + } + } + None => {} + } + } +} + +#[cfg(all(test, not(target_arch = "wasm32")))] +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 + /// 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(thread_per_subscription_spawner()); + let slow_stream: SubscriptionStream = Box::pin(stream::pending()); + manager.register("p:1".to_string(), 99, 98, 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(thread_per_subscription_spawner()); + let items = dummy_stream(vec![vec![0xaa], vec![0xbb]]); + manager.register("p:1".to_string(), 99, 98, 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.id, 99); + assert_eq!(frames[0].payload.value, vec![0xaa]); + assert_eq!(frames[1].payload.id, 99); + assert_eq!(frames[1].payload.value, vec![0xbb]); + assert_eq!(frames[2].payload.id, 98); + 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(thread_per_subscription_spawner()); + let slow_stream: SubscriptionStream = Box::pin(stream::pending()); + manager.register("p:1".to_string(), 99, 98, 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" + ); + } + + /// 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(), 99, 98, 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", + ); + } + + /// A `_stop` arriving before `activate` (the stop-before-register race on + /// non-serialized transports) must abort the subscription: no `_receive` + /// frames are emitted even though the stream had items to yield. + #[test] + fn stop_before_activate_aborts_subscription() { + let transport_typed = Arc::new(RecordingTransport::new()); + let transport_dyn: Arc = transport_typed.clone(); + let manager = SubscriptionManager::new(thread_per_subscription_spawner()); + let token = manager.reserve("p:1".to_string()); + manager.handle_stop("p:1"); + let items = dummy_stream(vec![vec![0x01], vec![0x02]]); + manager.activate(token, 99, 98, items, transport_dyn); + std::thread::sleep(std::time::Duration::from_millis(50)); + assert!( + transport_typed.sent().is_empty(), + "a stop before activate must abort the subscription" + ); + } + + /// Re-using a live request id (the duplicate-`_start` case) supersedes the + /// previous subscription rather than leaking it: the first stream is + /// stopped, only the second runs, and the superseded stream leaves no + /// frames behind. + #[test] + fn duplicate_start_supersedes_previous_without_leak() { + let transport_typed = Arc::new(RecordingTransport::new()); + let transport_dyn: Arc = transport_typed.clone(); + let manager = SubscriptionManager::new(thread_per_subscription_spawner()); + + // First subscription never yields; the second reservation for the + // same id must stop it. + let pending: SubscriptionStream = Box::pin(stream::pending()); + manager.register("p:1".to_string(), 99, 98, pending, transport_dyn.clone()); + + // Second subscription yields one item then ends. + let items = dummy_stream(vec![vec![0xaa]]); + manager.register("p:1".to_string(), 99, 98, items, transport_dyn); + + // Exactly the second stream's frames appear: one receive + one + // completion interrupt. The first (pending) stream contributes none. + let observed = transport_typed.wait_for(2, std::time::Duration::from_secs(2)); + assert_eq!( + observed, 2, + "expected the second stream's receive + interrupt only" + ); + let frames = transport_typed.sent(); + assert_eq!(frames[0].payload.id, 99); + assert_eq!(frames[0].payload.value, vec![0xaa]); + assert_eq!(frames[1].payload.id, 98); + + manager.handle_stop("p:1"); + std::thread::sleep(std::time::Duration::from_millis(50)); + assert_eq!( + transport_typed.sent().len(), + 2, + "no leaked frames from the superseded stream" + ); + } +} 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/tests/golden_frame.rs b/rust/crates/truapi-server/tests/golden_frame.rs new file mode 100644 index 00000000..24dd21fa --- /dev/null +++ b/rust/crates/truapi-server/tests/golden_frame.rs @@ -0,0 +1,53 @@ +//! 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::generated::wire_table; +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 { + id: wire_table::ACCOUNT_GET_ACCOUNT.request_id, + 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_table_ts_parity.rs b/rust/crates/truapi-server/tests/wire_table_ts_parity.rs new file mode 100644 index 00000000..e349139c --- /dev/null +++ b/rust/crates/truapi-server/tests/wire_table_ts_parity.rs @@ -0,0 +1,228 @@ +//! 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, unless +//! `TRUAPI_REQUIRE_GENERATED_TS=1` is set (CI sets it after running codegen), +//! in which case the missing file is a hard failure. + +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, + /// Subscription `_stop` / `_interrupt` ids; `None` for request methods. + stop: Option, + interrupt: Option, + is_subscription: bool, +} + +/// Parse a wire id. A malformed id is a hard failure, never a silent `0`: a +/// defensive fallback here would let a symmetric codegen-format change collapse +/// both tables to `0`s and pass the parity check while real drift slipped by. +fn parse_id(raw: &str, method: &str) -> u8 { + raw.trim_end_matches(',') + .trim() + .parse() + .unwrap_or_else(|_| panic!("unparseable wire id for `{method}`: {raw:?}")) +} + +fn parse_rust(src: &str) -> Vec { + // The Rust codegen emits one named `pub const FOO_BAR: RequestFrameIds = ...` + // (or `SubscriptionFrameIds`) per method. The const name is + // `SCREAMING_SNAKE_CASE` of the method name; we lowercase it to match the + // TS const names. This mirrors `parse_ts` below. + let mut out = Vec::new(); + let mut iter = src.lines(); + while let Some(line) = iter.next() { + let trimmed = line.trim(); + let Some(rest) = trimmed.strip_prefix("pub const ") else { + continue; + }; + let Some(colon) = rest.find(':') else { + continue; + }; + let is_subscription = rest.contains("SubscriptionFrameIds"); + // Skip non-id consts (e.g. `WIRE_TABLE: &[WireEntry]`). + if !is_subscription && !rest.contains("RequestFrameIds") { + continue; + } + let method = rest[..colon].trim().to_ascii_lowercase(); + let mut request_or_start = None; + let mut response_or_receive = None; + let mut stop = None; + let mut interrupt = None; + for inner in iter.by_ref() { + let t = inner.trim(); + if t.starts_with("};") { + break; + } + if let Some(rest) = t + .strip_prefix("request_id: ") + .or_else(|| t.strip_prefix("start_id: ")) + { + request_or_start = Some(parse_id(rest, &method)); + } + if let Some(rest) = t + .strip_prefix("response_id: ") + .or_else(|| t.strip_prefix("receive_id: ")) + { + response_or_receive = Some(parse_id(rest, &method)); + } + if let Some(rest) = t.strip_prefix("stop_id: ") { + stop = Some(parse_id(rest, &method)); + } + if let Some(rest) = t.strip_prefix("interrupt_id: ") { + interrupt = Some(parse_id(rest, &method)); + } + } + if let (Some(rs), Some(rr)) = (request_or_start, response_or_receive) { + out.push(Row { + method, + request_or_start: rs, + response_or_receive: rr, + stop, + interrupt, + is_subscription, + }); + } + } + out +} + +fn parse_ts(src: &str) -> Vec { + // The TS codegen emits one named `export const FOO_BAR = { ... }` per + // method. The const name is `SCREAMING_SNAKE_CASE` of the method name; + // we lowercase it to match the Rust `method:` strings. + 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("export const ") else { + continue; + }; + let Some(name_end) = rest.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_')) else { + continue; + }; + let method = rest[..name_end].to_ascii_lowercase(); + let mut request_or_start = None; + let mut response_or_receive = None; + let mut stop = None; + let mut interrupt = None; + let mut is_subscription = false; + for inner in iter.by_ref() { + let t = inner.trim(); + if t.starts_with("start:") || t.contains("SubscriptionFrameIds") { + is_subscription = true; + } + if let Some(rest) = t + .strip_prefix("request: ") + .or_else(|| t.strip_prefix("start: ")) + { + request_or_start = Some(parse_id(rest, &method)); + } + if let Some(rest) = t + .strip_prefix("response: ") + .or_else(|| t.strip_prefix("receive: ")) + { + response_or_receive = Some(parse_id(rest, &method)); + } + if let Some(rest) = t.strip_prefix("stop: ") { + stop = Some(parse_id(rest, &method)); + } + if let Some(rest) = t.strip_prefix("interrupt: ") { + interrupt = Some(parse_id(rest, &method)); + } + if t.starts_with("} as const") || 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, + stop, + interrupt, + 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 require_ts = std::env::var("TRUAPI_REQUIRE_GENERATED_TS").as_deref() == Ok("1"); + + let ts_path = match ts_path { + Ok(p) => p, + Err(_) => { + assert!( + !require_ts, + "TRUAPI_REQUIRE_GENERATED_TS=1 but wire-table.ts is missing; run scripts/codegen.sh" + ); + eprintln!( + "skipping wire-table parity check: TS wire-table.ts is not present \ + (run scripts/codegen.sh to generate it)" + ); + return; + } + }; + + let ts_src = match std::fs::read_to_string(&ts_path) { + Ok(s) => s, + Err(_) => { + assert!( + !require_ts, + "TRUAPI_REQUIRE_GENERATED_TS=1 but {} is unreadable", + ts_path.display() + ); + 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); + // Lower bound pinned to the known table size so a parser/codegen regression + // that quietly shrinks both tables in lockstep cannot pass: `assert_eq!` + // alone is satisfied by two equal-but-truncated tables. + const MIN_EXPECTED_ROWS: usize = 60; + assert!( + rust_rows.len() >= MIN_EXPECTED_ROWS, + "rust parser produced {} entries (expected >= {MIN_EXPECTED_ROWS}); \ + wire_table.rs format may have changed", + rust_rows.len() + ); + assert!( + ts_rows.len() >= MIN_EXPECTED_ROWS, + "ts parser produced {} entries (expected >= {MIN_EXPECTED_ROWS}); \ + wire-table.ts format may have changed", + ts_rows.len() + ); + 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/scripts/codegen.sh b/scripts/codegen.sh index 98d7006b..55ae2028 100755 --- a/scripts/codegen.sh +++ b/scripts/codegen.sh @@ -7,6 +7,7 @@ # --output js/packages/truapi/src/generated # --playground-output js/packages/truapi/src/playground # --client-examples-output playground/test/generated/examples +# --rust-output rust/crates/truapi-server/src/generated # --codec-version 1 # # The client surface defaults to the latest wire version any versioned @@ -25,9 +26,12 @@ cargo run -p truapi-codegen -- \ --output js/packages/truapi/src/generated \ --playground-output js/packages/truapi/src/playground \ --client-examples-output playground/test/generated/examples \ + --rust-output rust/crates/truapi-server/src/generated \ --explorer-output js/packages/truapi/src/explorer \ --codec-version 1 +rustfmt --edition 2024 rust/crates/truapi-server/src/generated/*.rs + node scripts/regen-explorer-versions.mjs npm exec --yes -- prettier --write \ @@ -58,3 +62,4 @@ fi echo "Generated client at js/packages/truapi/src/generated/" echo "Generated playground metadata at js/packages/truapi/src/playground/codegen/" echo "Generated client examples at playground/test/generated/examples/" +echo "Generated Rust dispatcher at rust/crates/truapi-server/src/generated/" From 2d67d3d7ef6c4df3ff419e3b701ff3b82f8f9314 Mon Sep 17 00:00:00 2001 From: w Date: Tue, 30 Jun 2026 22:17:01 -0400 Subject: [PATCH 2/2] fix: harden runtime boundary checks --- .github/workflows/ci.yml | 7 ++++ .github/workflows/deploy-docs.yml | 1 + .github/workflows/deploy-playground.yml | 1 + .github/workflows/release.yml | 1 + CLAUDE.md | 4 +-- Cargo.lock | 32 ------------------- Makefile | 2 +- README.md | 10 +++--- rust/crates/truapi-codegen/README.md | 13 +++++--- rust/crates/truapi-server/Cargo.toml | 1 - rust/crates/truapi-server/src/dispatcher.rs | 2 -- rust/crates/truapi-server/src/subscription.rs | 10 +++--- scripts/codegen.sh | 4 +-- 13 files changed, 33 insertions(+), 55 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5652e58..d2009d95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,6 +76,7 @@ jobs: - uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # nightly with: toolchain: nightly + components: rustfmt - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 @@ -89,6 +90,12 @@ jobs: - name: Run codegen run: ./scripts/codegen.sh + - name: Check generated Rust output is committed + run: git diff --exit-code -- rust/crates/truapi-server/src/generated + + - name: Check Rust/TS wire table parity + run: TRUAPI_REQUIRE_GENERATED_TS=1 cargo test -p truapi-server --test wire_table_ts_parity + - name: Upload codegen output uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 07aad965..8335a8c2 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -40,6 +40,7 @@ jobs: - uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # nightly with: toolchain: nightly + components: rustfmt - name: Install workspace dependencies run: npm ci diff --git a/.github/workflows/deploy-playground.yml b/.github/workflows/deploy-playground.yml index 83c8a1ad..111b0e3a 100644 --- a/.github/workflows/deploy-playground.yml +++ b/.github/workflows/deploy-playground.yml @@ -42,6 +42,7 @@ jobs: - uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # nightly with: toolchain: nightly + components: rustfmt - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 835f03fc..a24c7a15 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,6 +55,7 @@ jobs: uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # nightly with: toolchain: nightly + components: rustfmt - if: steps.version.outputs.proceed == 'true' uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # stable diff --git a/CLAUDE.md b/CLAUDE.md index 03a5659e..aa0787a7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ js/packages/ playground/ Next.js interactive playground; deploys to truapi-playground.dot hosts/dotli/ dotli submodule docs/ design docs, RFCs, feature proposals -scripts/codegen.sh regenerate the TS client from the Rust crate +scripts/codegen.sh regenerate TS client + Rust runtime tables from the Rust crate ``` ## Code style @@ -46,7 +46,7 @@ git submodule update --init --recursive ( cd playground && yarn install --frozen-lockfile ) ``` -## Regenerating the TS client +## Regenerating generated sources When the Rust trait surface changes, rerun: diff --git a/Cargo.lock b/Cargo.lock index eba92e34..b0bbd471 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -656,37 +656,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - [[package]] name = "truapi" version = "0.3.1" @@ -727,7 +696,6 @@ version = "0.1.0" dependencies = [ "futures", "parity-scale-codec", - "tracing", "truapi", ] diff --git a/Makefile b/Makefile index c8bc371c..be4de408 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ build: ## Build the Rust workspace and the TypeScript client. cargo build --workspace cd $(TRUAPI_PKG) && npm run build -codegen: ## Regenerate the TypeScript client from the Rust crate. +codegen: ## Regenerate the TypeScript client and Rust runtime tables from the Rust crate. ./scripts/codegen.sh cd $(PLAYGROUND) && rm -rf node_modules/@parity && yarn install diff --git a/README.md b/README.md index d8755190..ec042429 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ js/packages/ playground/ Interactive Next.js playground (truapi-playground.dot) hosts/dotli/ dotli host, vendored as a submodule docs/ Design docs, RFCs, feature proposals -scripts/codegen.sh Regenerate the TS client from the Rust source +scripts/codegen.sh Regenerate TS client + Rust runtime tables from Rust source ``` ## How it works @@ -95,16 +95,18 @@ yarn dev Open `https://dot.li/localhost:3000` inside the Polkadot Desktop Host. See [`playground/README.md`](playground/README.md) for deployment. -## Regenerate the TypeScript client +## Regenerate generated sources When the Rust trait surface changes: ```bash -make codegen # regenerate the TS client and refresh the playground snapshot +make codegen # regenerate the TS client, Rust runtime tables, and playground snapshot make playground # rebuild the playground against the refreshed snapshot ``` -This repopulates the ignored generated TS under `js/packages/truapi/`, including the playground metadata. +This repopulates the ignored generated TS under `js/packages/truapi/`, including +the playground metadata, and refreshes the checked-in Rust dispatcher and wire +table under `rust/crates/truapi-server/src/generated/`. ## Protocol versions diff --git a/rust/crates/truapi-codegen/README.md b/rust/crates/truapi-codegen/README.md index e924f090..e8eb1521 100644 --- a/rust/crates/truapi-codegen/README.md +++ b/rust/crates/truapi-codegen/README.md @@ -1,14 +1,15 @@ # truapi-codegen -_Reads rustdoc JSON for the `truapi` crate and generates the TypeScript client._ +_Reads rustdoc JSON for the `truapi` crate and generates client and runtime code._ [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](../../../LICENSE) -`truapi-codegen` keeps the generated client aligned with the Rust protocol definition. It reads rustdoc JSON, extracts the TrUAPI API surface, and writes: +`truapi-codegen` keeps generated code aligned with the Rust protocol definition. It reads rustdoc JSON, extracts the TrUAPI API surface, and writes: - TypeScript types for every protocol type in `truapi`. - TypeScript domain client classes for every unified trait. - The TypeScript wire dispatch table. +- The Rust host dispatcher and wire dispatch table consumed by `truapi-server`. ## Generated output @@ -50,7 +51,7 @@ The generator runs in three stages: 1. **Parse**: read JSON emitted by nightly rustdoc. 2. **Normalize**: extract the API model, including each method's `#[wire(id = N)]`. -3. **Emit**: generators write TypeScript output. +3. **Emit**: generators write TypeScript client output and, when requested, Rust runtime output. Missing or duplicate wire ids fail generation. Subscription methods reserve four consecutive ids for `_start`, `_stop`, `_interrupt`, and `_receive`. @@ -60,7 +61,8 @@ Missing or duplicate wire ids fail generation. Subscription methods reserve four cargo run -p truapi-codegen -- \ --input target/doc/truapi.json \ --output js/packages/truapi/src/generated \ - --version V2 \ + --rust-output rust/crates/truapi-server/src/generated \ + --client-version V2 \ --codec-version 1 ``` @@ -71,7 +73,8 @@ cargo +nightly rustdoc -p truapi -- -Z unstable-options --output-format json cargo run -p truapi-codegen -- \ --input target/doc/truapi.json \ --output js/packages/truapi/src/generated \ - --version V2 \ + --rust-output rust/crates/truapi-server/src/generated \ + --client-version V2 \ --codec-version 1 ``` diff --git a/rust/crates/truapi-server/Cargo.toml b/rust/crates/truapi-server/Cargo.toml index 138e4212..c92b76f9 100644 --- a/rust/crates/truapi-server/Cargo.toml +++ b/rust/crates/truapi-server/Cargo.toml @@ -9,4 +9,3 @@ description = "TrUAPI host runtime: frames, dispatcher, and subscriptions" truapi = { path = "../truapi" } futures = "0.3" parity-scale-codec = { version = "3", features = ["derive"] } -tracing = "0.1" diff --git a/rust/crates/truapi-server/src/dispatcher.rs b/rust/crates/truapi-server/src/dispatcher.rs index 20389abd..139655cb 100644 --- a/rust/crates/truapi-server/src/dispatcher.rs +++ b/rust/crates/truapi-server/src/dispatcher.rs @@ -10,7 +10,6 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; use futures::future::LocalBoxFuture; -use tracing::instrument; use crate::frame::{Payload, ProtocolMessage}; use crate::generated::wire_table::{RequestFrameIds, SubscriptionFrameIds}; @@ -117,7 +116,6 @@ impl Dispatcher { /// Process an incoming protocol message, sending any responses or /// subscription frames through `transport`. A discriminant with no /// registered handler is dropped. - #[instrument(skip_all, fields(runtime.method = "dispatcher.dispatch"))] pub async fn dispatch(&self, message: ProtocolMessage, transport: Arc) { let id = message.payload.id; diff --git a/rust/crates/truapi-server/src/subscription.rs b/rust/crates/truapi-server/src/subscription.rs index 8b24fd98..f54b8cd5 100644 --- a/rust/crates/truapi-server/src/subscription.rs +++ b/rust/crates/truapi-server/src/subscription.rs @@ -27,12 +27,10 @@ type StopFn = Box; /// 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 { +/// Test spawner that starts a fresh OS thread per subscription and drives the +/// future with `futures::executor::block_on`. +#[cfg(all(test, not(target_arch = "wasm32")))] +pub(crate) fn thread_per_subscription_spawner() -> Spawner { Arc::new(|fut: BoxFuture<'static, ()>| { std::thread::spawn(move || futures::executor::block_on(fut)); }) diff --git a/scripts/codegen.sh b/scripts/codegen.sh index 55ae2028..e41cf0eb 100755 --- a/scripts/codegen.sh +++ b/scripts/codegen.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Regenerate js/packages/truapi/src/generated/* from rust/crates/truapi. +# Regenerate generated TypeScript and Rust runtime sources from rust/crates/truapi. # # Pipeline: # 1. cargo +nightly rustdoc -p truapi --output-format json -> target/doc/truapi.json @@ -30,7 +30,7 @@ cargo run -p truapi-codegen -- \ --explorer-output js/packages/truapi/src/explorer \ --codec-version 1 -rustfmt --edition 2024 rust/crates/truapi-server/src/generated/*.rs +rustfmt +nightly --edition 2024 rust/crates/truapi-server/src/generated/*.rs node scripts/regen-explorer-versions.mjs