From 153d37c3ba6f7332a2882e12ace5eb6082592f55 Mon Sep 17 00:00:00 2001 From: pgherveou Date: Wed, 20 May 2026 21:21:12 +0000 Subject: [PATCH 1/5] refactor: small cleanups extracted from #96 prep work Three independently-mergeable cleanups identified while preparing the Rust core runtime port (#96): - `truapi/transport.ts`: extract a module-level `toError(unknown): Error` helper and use it at the three sites that hand-rolled the same `error instanceof Error ? error : new Error(String(error))` coercion. - `truapi-codegen::MethodKind`: derive `Clone` and `Copy` so the enum can be passed through helpers without re-matching. Trivial additive change; the variants are bare. - `rust/crates/truapi/README.md`: drop the spurious `v02` mention from the bulleted module list. The table below it (and the rest of the document) only mentions `v01`; the prose was out of date. --- js/packages/truapi/src/transport.ts | 13 ++++++++++--- rust/crates/truapi-codegen/src/rustdoc.rs | 2 +- rust/crates/truapi/README.md | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/js/packages/truapi/src/transport.ts b/js/packages/truapi/src/transport.ts index 78262a99..8a92133e 100644 --- a/js/packages/truapi/src/transport.ts +++ b/js/packages/truapi/src/transport.ts @@ -2,6 +2,13 @@ import { err, ok, type Result, type ResultAsync } from "neverthrow"; import { str, u8, type ResultPayload } from "./scale.js"; +/** + * Coerce an unknown thrown value into an `Error` instance. + */ +function toError(error: unknown): Error { + return error instanceof Error ? error : new Error(String(error)); +} + /** * Handle returned by TrUAPI subscription APIs. **/ @@ -408,7 +415,7 @@ function createBaseProvider() { /** Transition to the closed state. Idempotent. */ close(error: unknown) { if (closedError) return; - closedError = error instanceof Error ? error : new Error(String(error)); + closedError = toError(error); for (const fn of [...onCloseCleanup]) { try { fn(); @@ -494,7 +501,7 @@ export function createIframeProvider(options: { target.postMessage(message, hostOrigin); } catch (error) { base.close(error); - throw error instanceof Error ? error : new Error(String(error)); + throw toError(error); } }, subscribe: base.subscribe, @@ -559,7 +566,7 @@ export function createMessagePortProvider( resolvedPort.postMessage(message); } catch (error) { base.close(error); - throw error instanceof Error ? error : new Error(String(error)); + throw toError(error); } } else { pending.push(message); diff --git a/rust/crates/truapi-codegen/src/rustdoc.rs b/rust/crates/truapi-codegen/src/rustdoc.rs index a9179e0a..fbbc7d68 100644 --- a/rust/crates/truapi-codegen/src/rustdoc.rs +++ b/rust/crates/truapi-codegen/src/rustdoc.rs @@ -92,7 +92,7 @@ pub struct WireAttrs { } /// Wire-shape classification of a trait method. -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MethodKind { /// One request, one response. Request, diff --git a/rust/crates/truapi/README.md b/rust/crates/truapi/README.md index 5a5b4737..f033f88d 100644 --- a/rust/crates/truapi/README.md +++ b/rust/crates/truapi/README.md @@ -8,7 +8,7 @@ _Source of truth for the TrUAPI protocol: shared traits, versioned types, and th It defines: -- **Versioned data types** under `v01`, `v02`, and `versioned`. +- **Versioned data types** under `v01` and `versioned`. - **Domain API traits** under `api/`, plus the composed `TrUApi` trait. - **Wire ids** via per-method `#[wire(id = N)]` annotations that pin the byte-level method table. - **Subscription primitives** through `Subscription` for streamed host responses. From 2764c52c3c47777ffc755869f5f9d9a83c912f5c Mon Sep 17 00:00:00 2001 From: pgherveou Date: Wed, 20 May 2026 21:28:08 +0000 Subject: [PATCH 2/5] docs(claude.md): add four style rules surfaced during #96 prep work - Don't introduce typealias chains that just rename a type from another crate; use the canonical name directly. A typealias should capture a real abstraction, not just a rename. - After any code change, update README.md (and CLAUDE.md if the layout changed) so the top-level docs match the repo. Stale docs are a regression. - In codegen emitters, prefer indoc::writedoc! / formatdoc! over chains of writeln!. Multi-line raw strings keep the emitted shape visible in source instead of fragmenting it. - In PR descriptions, issue comments, and other artifacts that outlive the conversation, describe the resulting state, not the transition between commits. "Previously X, now Y" framing reads as ephemeral history once the PR is squash-merged. --- CLAUDE.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 11187e79..b7240226 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,6 +27,10 @@ scripts/codegen.sh regenerate the TS client from the Rust crate - Remove legacy compatibility code by default. Keep or add it only when explicitly requested. - In Rust format strings, prefer inlined variables: `"log value: {value:?}"` over `"log value: {:?}", value`. - **No `any` in TypeScript types**: If a type can't be expressed cleanly, stop and ask the user whether to (a) refactor or import the right type or (b) add a scoped `// eslint-disable-next-line @typescript-eslint/no-explicit-any` exception. Never silently leave `any`. +- Don't introduce typealias chains that just rename a public type from another crate (e.g. `pub type StorageError = crate::v01::HostLocalStorageReadError`). Use the canonical name directly. A typealias is only worth its indirection when it captures a real abstraction. +- After any code change, update `README.md` (and CLAUDE.md if the layout changed) so the top-level docs reflect what the repo actually contains. Stale docs are a regression. +- In codegen emitters, prefer `indoc::writedoc!` / `formatdoc!` over chains of `writeln!`. A single `writedoc!` with a multi-line raw string keeps the emitted shape visible in source instead of fragmenting it across one-line `writeln!` calls. Reserve `writeln!` for the genuinely-one-line case (a single import, a single statement inside a loop). +- In PR descriptions, issue comments, and other artifacts that outlive the conversation: describe the resulting state, not the transition between commits. Avoid "previously X, now Y", "we removed", "the old shim is gone", "this PR replaces", those read as ephemeral history once the PR is squash-merged. Write what the system *does* after the change, not what each commit *changed* on the way there. (Commit messages are the place for transition narrative; they survive in `git log` even after the squash.) ## First-time setup From 44efef56d0a60aaecfa58be922479a539eb72b54 Mon Sep 17 00:00:00 2001 From: pgherveou Date: Wed, 20 May 2026 21:29:59 +0000 Subject: [PATCH 3/5] feat(truapi): derive Display on permission request types Permission request enums are surfaced to host UIs through the prompt flow, where they need human-readable rendering. The versioned wrappers and the inner v01 enums now implement Display through `derive_more`: - `v01::HostDevicePermissionRequest` formats each variant with a short label ("camera", "open URL", "NFC", ...). - `v01::RemotePermission` formats `Remote { domains }` as "access to " and each named variant with its own phrase ("submit chain transactions", "WebRTC connections", ...). - `v01::RemotePermissionRequest` formats the `permissions` vec joined with "; ". - The versioned wrappers `HostDevicePermissionRequest` and `RemotePermissionRequest` forward through `{_0}` so callers can format either layer. Internal `DisplayDomains` and `DisplayPermissions` adapters write straight into the `Formatter`, avoiding the intermediate `Vec` + `join` allocations a `format_*` helper would incur. The `versioned_type!` macro is extended to thread outer enum and inner variant `#[meta]` attributes so the `derive(Display)` lands on the generated code. --- Cargo.lock | 50 +++++++++++++++- rust/crates/truapi/Cargo.toml | 1 + rust/crates/truapi/src/v01/permissions.rs | 60 ++++++++++++++++++- rust/crates/truapi/src/versioned/mod.rs | 10 ++++ .../truapi/src/versioned/permissions.rs | 4 ++ 5 files changed, 121 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 26ca7750..dabc1fb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -158,6 +158,38 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -412,12 +444,27 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -524,6 +571,7 @@ dependencies = [ name = "truapi" version = "0.1.0" dependencies = [ + "derive_more", "futures", "hex", "parity-scale-codec", @@ -536,7 +584,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", - "convert_case", + "convert_case 0.6.0", "indoc", "serde", "serde_json", diff --git a/rust/crates/truapi/Cargo.toml b/rust/crates/truapi/Cargo.toml index b5d2bef8..f47da511 100644 --- a/rust/crates/truapi/Cargo.toml +++ b/rust/crates/truapi/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true description = "TrUAPI trait and type definitions" [dependencies] +derive_more = { version = "2", features = ["display"] } futures = "0.3" hex = "0.4" parity-scale-codec = { version = "3", features = ["derive"] } diff --git a/rust/crates/truapi/src/v01/permissions.rs b/rust/crates/truapi/src/v01/permissions.rs index f56bb536..ab06ab3c 100644 --- a/rust/crates/truapi/src/v01/permissions.rs +++ b/rust/crates/truapi/src/v01/permissions.rs @@ -1,3 +1,6 @@ +use core::fmt; + +use derive_more::Display; use parity_scale_codec::{Decode, Encode}; /// Device-capability permission requested from the host (RFC 0002). @@ -5,17 +8,26 @@ use parity_scale_codec::{Decode, Encode}; /// The user's decision is persisted indefinitely after the first prompt and /// survives app restarts, whether the decision was grant or deny; the host /// does not re-prompt on subsequent requests for the same capability. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, Display)] #[allow(clippy::upper_case_acronyms)] pub enum HostDevicePermissionRequest { + #[display("notifications")] Notifications, + #[display("camera")] Camera, + #[display("microphone")] Microphone, + #[display("bluetooth")] Bluetooth, + #[display("NFC")] NFC, + #[display("location")] Location, + #[display("clipboard")] Clipboard, + #[display("open URL")] OpenUrl, + #[display("biometrics")] Biometrics, } @@ -23,30 +35,72 @@ pub enum HostDevicePermissionRequest { /// /// `ChainSubmit`, `PreimageSubmit`, and `StatementSubmit` are also triggered /// implicitly by the corresponding business calls when not yet granted. -#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, Display)] pub enum RemotePermission { /// Outbound HTTP/WebSocket access to a set of domains. + #[display("access to {}", DisplayDomains(domains))] Remote { /// Domain patterns requested by the product. domains: Vec, }, /// WebRTC media access. + #[display("WebRTC connections")] WebRtc, /// Submitting transactions on behalf of the user via `remote_chain_transaction_broadcast`. + #[display("submit chain transactions")] ChainSubmit, /// Submitting preimages on behalf of the user via `remote_preimage_submit`. + #[display("submit preimages")] PreimageSubmit, /// Submitting statements on behalf of the user via `remote_statement_store_submit`. + #[display("submit statements")] StatementSubmit, } +struct DisplayDomains<'a>(&'a [String]); + +impl fmt::Display for DisplayDomains<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.0.is_empty() { + return f.write_str("(no domains)"); + } + let mut sorted: Vec<&str> = self.0.iter().map(String::as_str).collect(); + sorted.sort(); + for (i, domain) in sorted.iter().enumerate() { + if i > 0 { + f.write_str(", ")?; + } + f.write_str(domain)?; + } + Ok(()) + } +} + /// Batched remote-permission request (RFC 0002). -#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, Display)] +#[display("{}", DisplayPermissions(permissions))] pub struct RemotePermissionRequest { /// Permissions requested by the product. pub permissions: Vec, } +struct DisplayPermissions<'a>(&'a [RemotePermission]); + +impl fmt::Display for DisplayPermissions<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.0.is_empty() { + return f.write_str("(empty)"); + } + for (i, permission) in self.0.iter().enumerate() { + if i > 0 { + f.write_str("; ")?; + } + write!(f, "{permission}")?; + } + Ok(()) + } +} + /// Outcome of a device-permission request. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct HostDevicePermissionResponse { diff --git a/rust/crates/truapi/src/versioned/mod.rs b/rust/crates/truapi/src/versioned/mod.rs index 116c88cf..f7b12fd4 100644 --- a/rust/crates/truapi/src/versioned/mod.rs +++ b/rust/crates/truapi/src/versioned/mod.rs @@ -24,6 +24,7 @@ pub trait IntoVersion: Sized { macro_rules! versioned_type { ( $( + $(#[$enum_meta:meta])* pub enum $name:ident { $($body:tt)* } @@ -32,6 +33,7 @@ macro_rules! versioned_type { $( versioned_type! { @one + $(#[$enum_meta])* pub enum $name { $($body)* } @@ -41,13 +43,17 @@ macro_rules! versioned_type { ( @one + $(#[$enum_meta:meta])* pub enum $name:ident { + $(#[$variant_meta:meta])* V1 => $v1:ty $(,)? } ) => { + $(#[$enum_meta])* #[doc = concat!("Versioned envelope for [`", stringify!($name), "`].")] #[derive(Debug, Clone, PartialEq, Eq, parity_scale_codec::Encode, parity_scale_codec::Decode)] pub enum $name { + $(#[$variant_meta])* #[codec(index = 0)] V1($v1), } @@ -61,13 +67,17 @@ macro_rules! versioned_type { ( @one + $(#[$enum_meta:meta])* pub enum $name:ident { + $(#[$variant_meta:meta])* V1 $(,)? } ) => { + $(#[$enum_meta])* #[doc = concat!("Versioned envelope for [`", stringify!($name), "`].")] #[derive(Debug, Clone, PartialEq, Eq, parity_scale_codec::Encode, parity_scale_codec::Decode)] pub enum $name { + $(#[$variant_meta])* #[codec(index = 0)] V1, } diff --git a/rust/crates/truapi/src/versioned/permissions.rs b/rust/crates/truapi/src/versioned/permissions.rs index 71558a5d..53e97079 100644 --- a/rust/crates/truapi/src/versioned/permissions.rs +++ b/rust/crates/truapi/src/versioned/permissions.rs @@ -3,9 +3,13 @@ use crate::v01; versioned_type! { + #[derive(derive_more::Display)] + #[display("{_0}")] pub enum HostDevicePermissionRequest { V1 => v01::HostDevicePermissionRequest } pub enum HostDevicePermissionResponse { V1 => v01::HostDevicePermissionResponse } pub enum HostDevicePermissionError { V1 => v01::GenericError } + #[derive(derive_more::Display)] + #[display("{_0}")] pub enum RemotePermissionRequest { V1 => v01::RemotePermissionRequest } pub enum RemotePermissionResponse { V1 => v01::RemotePermissionResponse } pub enum RemotePermissionError { V1 => v01::GenericError } From cb8614ddf62d1f6f04a11ec161480f2c4271e3ae Mon Sep 17 00:00:00 2001 From: pgherveou Date: Mon, 25 May 2026 11:40:56 +0200 Subject: [PATCH 4/5] refactor(truapi): inline domains formatting in display attribute Drop the DisplayDomains helper and call domains.join(", ") directly from the #[display] template. --- rust/crates/truapi/src/v01/permissions.rs | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/rust/crates/truapi/src/v01/permissions.rs b/rust/crates/truapi/src/v01/permissions.rs index ab06ab3c..4b4a95f0 100644 --- a/rust/crates/truapi/src/v01/permissions.rs +++ b/rust/crates/truapi/src/v01/permissions.rs @@ -38,7 +38,7 @@ pub enum HostDevicePermissionRequest { #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, Display)] pub enum RemotePermission { /// Outbound HTTP/WebSocket access to a set of domains. - #[display("access to {}", DisplayDomains(domains))] + #[display("access to {}", domains.join(", "))] Remote { /// Domain patterns requested by the product. domains: Vec, @@ -57,25 +57,6 @@ pub enum RemotePermission { StatementSubmit, } -struct DisplayDomains<'a>(&'a [String]); - -impl fmt::Display for DisplayDomains<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.0.is_empty() { - return f.write_str("(no domains)"); - } - let mut sorted: Vec<&str> = self.0.iter().map(String::as_str).collect(); - sorted.sort(); - for (i, domain) in sorted.iter().enumerate() { - if i > 0 { - f.write_str(", ")?; - } - f.write_str(domain)?; - } - Ok(()) - } -} - /// Batched remote-permission request (RFC 0002). #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, Display)] #[display("{}", DisplayPermissions(permissions))] From bc5c6657636f6ab09a61d7fa8da1096887fae6e3 Mon Sep 17 00:00:00 2001 From: pgherveou Date: Tue, 26 May 2026 15:51:12 +0200 Subject: [PATCH 5/5] simplify --- rust/crates/truapi/src/v01/permissions.rs | 24 ++++------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/rust/crates/truapi/src/v01/permissions.rs b/rust/crates/truapi/src/v01/permissions.rs index 4b4a95f0..b847103e 100644 --- a/rust/crates/truapi/src/v01/permissions.rs +++ b/rust/crates/truapi/src/v01/permissions.rs @@ -1,5 +1,3 @@ -use core::fmt; - use derive_more::Display; use parity_scale_codec::{Decode, Encode}; @@ -59,29 +57,15 @@ pub enum RemotePermission { /// Batched remote-permission request (RFC 0002). #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, Display)] -#[display("{}", DisplayPermissions(permissions))] +#[display( + "{}", + permissions.iter().map(ToString::to_string).collect::>().join("; ") +)] pub struct RemotePermissionRequest { /// Permissions requested by the product. pub permissions: Vec, } -struct DisplayPermissions<'a>(&'a [RemotePermission]); - -impl fmt::Display for DisplayPermissions<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.0.is_empty() { - return f.write_str("(empty)"); - } - for (i, permission) in self.0.iter().enumerate() { - if i > 0 { - f.write_str("; ")?; - } - write!(f, "{permission}")?; - } - Ok(()) - } -} - /// Outcome of a device-permission request. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct HostDevicePermissionResponse {