From c94a07b42fb73354ce96d180b8c86a256c6bd49c Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Thu, 6 Aug 2026 14:00:44 -0300 Subject: [PATCH 1/8] RFC 0026: Host chain discovery and name resolution Add the RFC document plus its protocol surface: chain.getSupportedChains (wire id 166) enumerates the chains a host serves as (name, network, genesisHash) descriptors, and chain.resolveChain (168) maps a (name, network) pair to its genesis hash or NotFound. Both trait methods are stubs returning unavailable, so products stop hard-coding genesis hashes once hosts implement the backing syscall in a follow-up. --- docs/rfcs/0026-supported-chains.md | 175 ++++++++++++++++++ docs/rfcs/_index.md | 1 + .../truapi-codegen/tests/golden/dispatcher.rs | 58 +++++- .../truapi-codegen/tests/golden/wire_table.rs | 20 ++ .../truapi-server/src/generated/dispatcher.rs | 66 ++++++- .../truapi-server/src/generated/wire_table.rs | 20 ++ rust/crates/truapi/src/api/chain.rs | 46 ++++- rust/crates/truapi/src/v01/chain.rs | 45 +++++ rust/crates/truapi/src/versioned/chain.rs | 6 + 9 files changed, 431 insertions(+), 6 deletions(-) create mode 100644 docs/rfcs/0026-supported-chains.md diff --git a/docs/rfcs/0026-supported-chains.md b/docs/rfcs/0026-supported-chains.md new file mode 100644 index 000000000..742265d97 --- /dev/null +++ b/docs/rfcs/0026-supported-chains.md @@ -0,0 +1,175 @@ +--- +title: "Host chain discovery and name resolution" +owner: "@valentinfernandez1" +--- + +# RFC 0026: Host chain discovery and name resolution + +| | | +| --------------- | -------------------------------------------------------------------------------------------------- | +| **RFC Number** | 26 | +| **Start Date** | 2026-08-06 | +| **Description** | Two `Chain` methods letting products enumerate the chains a host serves and resolve stable names to genesis hashes. | +| **Authors** | Valentin Fernandez | + +## Summary + +Add two methods to the `Chain` trait. `get_supported_chains` returns the complete set of chains the host will serve, each as a descriptor carrying a stable machine name (for example `"asset-hub"`), an ecosystem network string (for example `"paseo"`), and the chain's genesis hash. `resolve_chain` maps one `(name, network)` pair to its genesis hash, or fails with `NotFound`. Both are answered in-core from a single new platform syscall, so each host implements exactly one callback over configuration it already has. + +## Motivation + +Every chain-scoped TrUAPI call is keyed by `genesisHash`, and today products obtain those hashes by hard-coding them: `@parity/truapi` ships constants like `PASEO_NEXT_V2_ASSET_HUB` in `well-known-chains.ts`, and the product SDK carries its own `WellKnownChain` table. Hard-coded hashes fail in three recurring ways. + +**Testnet wipes.** When a testnet is wiped or restarted its genesis hash changes. Every product's baked-in constant goes stale at once, and nothing recovers until each product ships a new bundle with the new hash. The host already knows the new hash the moment its own configuration updates, but products have no way to ask for it. + +**Guessing the host's network.** A product cannot ask which environment the host is on, so it guesses. If the product assumes one network and the host is configured for another, every chain call fails at runtime with no better diagnostic than an unsupported genesis hash. + +**Environment moves.** Pointing a product at a different environment (a new testnet iteration, a devnet) means editing constants and shipping a new build, even though the host-side change is a config edit. + +The fix is to make the host's chain set discoverable over the wire. Hosts already hold this data in enumerable form: dotli's network config has named slots (`relay`, `assethub`, `bulletin`, `people`) per environment, each with a genesis hash and RPC endpoints. This RFC exposes that mapping to products. Tracking issue: [paritytech/truapi#352](https://github.com/paritytech/truapi/issues/352). + +## Detailed Design + +### `chain.getSupportedChains` + +```rust +/// Enumerate the chains this host serves. +/// +/// ```ts +/// const result = await truapi.chain.getSupportedChains(); +/// assert(result.isOk(), "getSupportedChains failed:", result); +/// console.log("supported chains:", result.value.chains); +/// ``` +#[wire(request_id = 166)] +async fn get_supported_chains( + &self, + _cx: &CallContext, + _request: RemoteChainSupportedChainsRequest, +) -> Result> { + Err(CallError::unavailable()) +} +``` + +The request carries no payload (a payload-less `V1` envelope on the wire, a no-argument call in the TS client). The response: + +```rust +/// Response listing every chain the host serves. +struct RemoteChainSupportedChainsResponse { + /// Complete set of chains available through this host. + chains: Vec, +} + +/// One chain a host serves. +struct HostChainDescriptor { + /// Stable machine key for the chain's role, e.g. "asset-hub". + name: String, + /// Ecosystem the chain belongs to, e.g. "polkadot", "kusama", "paseo", "devnet". + network: String, + /// Genesis hash identifying the chain in all chain-scoped calls. + genesis_hash: Vec, +} +``` + +The error is the plain `GenericError` catch-all, matching the neighboring `getSpec*` methods. + +### `chain.resolveChain` + +```rust +/// Resolve a (name, network) pair to the chain's genesis hash. +/// +/// ```ts +/// const result = await truapi.chain.resolveChain({ +/// name: "asset-hub", +/// network: "paseo", +/// }); +/// assert(result.isOk(), "resolveChain failed:", result); +/// console.log("genesis hash:", result.value.genesisHash); +/// ``` +#[wire(request_id = 168)] +async fn resolve_chain( + &self, + _cx: &CallContext, + _request: RemoteChainResolveChainRequest, +) -> Result> { + Err(CallError::unavailable()) +} +``` + +```rust +/// Request to resolve a named chain within a network. +struct RemoteChainResolveChainRequest { + /// Stable machine key, e.g. "asset-hub". + name: String, + /// Ecosystem string, e.g. "polkadot", "paseo". + network: String, +} + +/// Response carrying the resolved genesis hash. +struct RemoteChainResolveChainResponse { + /// Genesis hash of the resolved chain. + genesis_hash: Vec, +} + +/// Error from resolve_chain. +enum RemoteChainResolveChainError { + /// No supported chain matches the requested (name, network) pair. + NotFound, + /// Catch-all. + Unknown(GenericError), +} +``` + +Both methods land in `v01` (the single unfrozen wire) beside the existing chain-metadata methods `getSpecGenesisHash` (94), `getSpecChainName` (96), and `getSpecProperties` (98), taking the next free wire ids, and are re-exported through `truapi::latest`. + +### Semantics and invariants + +- **Completeness.** The returned list is the complete set of chains the host will serve `chain.*` and `signing.*` calls for. A genesis hash absent from the list will not be served, and every listed hash will be. +- **Uniqueness.** `(name, network)` is unique within one host's response, so `resolve_chain` is a plain lookup with a single answer. +- **Name stability.** Names are stable machine keys across sessions. A product that persisted `"asset-hub"` resolves it again after any wipe and receives the current genesis hash. +- **Fixed per connection.** The list does not change for the lifetime of a connection. There is no subscription; a product observes host-side changes by reconnecting. + +`network` is an open ecosystem string, not a `Mainnet`/`Testnet` enum, because a binary flag cannot distinguish two testnets: a host serving both a Paseo asset hub and a devnet asset hub needs `("asset-hub", "paseo")` and `("asset-hub", "devnet")` to be different keys. + +Descriptors deliberately exclude display names and token properties. Once a product holds the genesis hash, that metadata is already reachable through `getSpecChainName` and `getSpecProperties`. + +### Typical product flow + +```ts +const supported = await truapi.chain.getSupportedChains(); +assert(supported.isOk(), "getSupportedChains failed:", supported); + +const hub = supported.value.chains.find((c) => c.name === "asset-hub"); +assert(hub !== undefined, "host serves no asset hub"); + +const name = await truapi.chain.getSpecChainName({ genesisHash: hub.genesisHash }); +assert(name.isOk(), "getSpecChainName failed:", name); +console.log("connected to:", name.value.chainName); +``` + +The product never embeds a hash. After a testnet wipe the host updates its config, the product reconnects, and the same code path picks up the new hash. + +### Implementation shape + +The core does not own the chain set. `system.featureSupported(Chain { genesis_hash })` is already a thin shim in `rust/crates/truapi-server/src/host_logic/features.rs` delegating to `truapi_platform::Features`, and `ChainProvider::connect(genesis_hash)` opens JSON-RPC pipes on demand. This RFC follows the same delegation pattern: + +- `truapi-platform` gains one syscall on `Features`, shaped like `supported_chains() -> Result, GenericError>`. +- `truapi-server` answers **both** wire methods in-core from that single syscall: `get_supported_chains` returns the list as-is, and `resolve_chain` filters it by `(name, network)`, mapping a miss to `NotFound`. + +Hosts therefore implement exactly one callback, backed by configuration they already maintain. dotli's per-environment named slots (`relay`, `assethub`, `bulletin`, `people`, each with a genesis hash) map directly onto descriptors; the iOS `TrUAPIHost` and the host CLI expose their equivalent config the same way. Because `resolve_chain` is answered in-core over the same data, the completeness invariant holds by construction: the two methods cannot disagree. + +The change is purely additive: two new methods with fresh wire ids, no changes to existing calls or types. Existing products keep working unchanged, including their hard-coded constants, and can migrate to discovery at their own pace. + +## Non-goals + +- Changing the `genesisHash` parameter on existing chain-scoped calls. Genesis hashes stay the wire-level chain identifier everywhere else. +- Product SDK integration. The SDK will wrap these calls behind its own chain-selection API in its own repo, hiding the raw methods from application code. + +## Drawbacks + +- Name and network strings are minted by host configuration, not by the protocol. Two hosts could use different names for the same chain until a spec-level registry exists (see Unresolved Questions). +- No change notification. A host that reconfigures mid-session cannot inform connected products; they observe the change only on reconnect. This keeps the API subscription-free and matches how host config changes actually roll out (host restarts). + +## Alternatives + +- **Take chain names instead of `genesisHash` in every chain-scoped call.** Was discartes as this is a breaking change across the Rust trait, codegen, the TS client, dotli, the iOS host, and the product SDK. The genesis hash also remains necessary internally, since connections are keyed by it and signed payloads embed it via `CheckGenesis`. +- **A protocol-defined closed enum of chains.** This was discarded as adding a chain would require a protocol release, which is exactly the coupling this RFC removes. diff --git a/docs/rfcs/_index.md b/docs/rfcs/_index.md index 15b95f4ea..105af6bd1 100644 --- a/docs/rfcs/_index.md +++ b/docs/rfcs/_index.md @@ -25,3 +25,4 @@ created: 2026-03-13 | 0021 | [Add Coins variant to PaymentTopUpSource](0021-payment-topup-coins.md) | accepted | @filippovecchiato | — | | 0022 | [Account key derivations](0022-account-derivations.md) | draft | Valentin Sergeev | — | | 0023 | [sr25519 VRF signing for product accounts](0023-account-sign-vrf.md) | draft | Valentin Sergeev | — | +| 0026 | [Host chain discovery and name resolution](0026-supported-chains.md) | draft | Valentin Fernandez | [#354](https://github.com/paritytech/truapi/pull/354) | diff --git a/rust/crates/truapi-codegen/tests/golden/dispatcher.rs b/rust/crates/truapi-codegen/tests/golden/dispatcher.rs index b11224efd..92341c319 100644 --- a/rust/crates/truapi-codegen/tests/golden/dispatcher.rs +++ b/rust/crates/truapi-codegen/tests/golden/dispatcher.rs @@ -598,7 +598,7 @@ where }); } { - let host = host; + let host = host.clone(); dispatcher.on_request(wire_table::CHAIN_STOP_TRANSACTION, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { @@ -625,6 +625,62 @@ where }) }); } + { + let host = host.clone(); + dispatcher.on_request(wire_table::CHAIN_GET_SUPPORTED_CHAINS, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainSupportedChainsRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainSupportedChainsResponse = match host.get_supported_chains(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::CHAIN_RESOLVE_CHAIN, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainResolveChainRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainResolveChainResponse = match host.resolve_chain(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } } fn register_chat

(dispatcher: &mut Dispatcher, host: Arc

) diff --git a/rust/crates/truapi-codegen/tests/golden/wire_table.rs b/rust/crates/truapi-codegen/tests/golden/wire_table.rs index 7360d0427..0aa3a9074 100644 --- a/rust/crates/truapi-codegen/tests/golden/wire_table.rs +++ b/rust/crates/truapi-codegen/tests/golden/wire_table.rs @@ -466,6 +466,18 @@ pub const ACCOUNT_SIGN_VRF: RequestFrameIds = RequestFrameIds { response_id: 165, }; +/// Wire discriminants for `chain_get_supported_chains`. +pub const CHAIN_GET_SUPPORTED_CHAINS: RequestFrameIds = RequestFrameIds { + request_id: 166, + response_id: 167, +}; + +/// Wire discriminants for `chain_resolve_chain`. +pub const CHAIN_RESOLVE_CHAIN: RequestFrameIds = RequestFrameIds { + request_id: 168, + response_id: 169, +}; + /// 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] = &[ @@ -729,4 +741,12 @@ pub const WIRE_TABLE: &[WireEntry] = &[ method: "account_sign_vrf", kind: WireKind::Request(ACCOUNT_SIGN_VRF), }, + WireEntry { + method: "chain_get_supported_chains", + kind: WireKind::Request(CHAIN_GET_SUPPORTED_CHAINS), + }, + WireEntry { + method: "chain_resolve_chain", + kind: WireKind::Request(CHAIN_RESOLVE_CHAIN), + }, ]; diff --git a/rust/crates/truapi-server/src/generated/dispatcher.rs b/rust/crates/truapi-server/src/generated/dispatcher.rs index 1cefce408..4a7cba817 100644 --- a/rust/crates/truapi-server/src/generated/dispatcher.rs +++ b/rust/crates/truapi-server/src/generated/dispatcher.rs @@ -718,7 +718,7 @@ where }); } { - let host = host; + let host = host.clone(); dispatcher.on_request(wire_table::CHAIN_STOP_TRANSACTION, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { @@ -745,6 +745,70 @@ where }) }); } + { + let host = host.clone(); + dispatcher.on_request(wire_table::CHAIN_GET_SUPPORTED_CHAINS, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainSupportedChainsRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainSupportedChainsResponse = match host.get_supported_chains(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request( + wire_table::CHAIN_RESOLVE_CHAIN, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainResolveChainRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::chain::RemoteChainResolveChainError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainResolveChainResponse = + match host.resolve_chain(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } } fn register_chat

(dispatcher: &mut Dispatcher, host: Arc

) diff --git a/rust/crates/truapi-server/src/generated/wire_table.rs b/rust/crates/truapi-server/src/generated/wire_table.rs index 7360d0427..0aa3a9074 100644 --- a/rust/crates/truapi-server/src/generated/wire_table.rs +++ b/rust/crates/truapi-server/src/generated/wire_table.rs @@ -466,6 +466,18 @@ pub const ACCOUNT_SIGN_VRF: RequestFrameIds = RequestFrameIds { response_id: 165, }; +/// Wire discriminants for `chain_get_supported_chains`. +pub const CHAIN_GET_SUPPORTED_CHAINS: RequestFrameIds = RequestFrameIds { + request_id: 166, + response_id: 167, +}; + +/// Wire discriminants for `chain_resolve_chain`. +pub const CHAIN_RESOLVE_CHAIN: RequestFrameIds = RequestFrameIds { + request_id: 168, + response_id: 169, +}; + /// 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] = &[ @@ -729,4 +741,12 @@ pub const WIRE_TABLE: &[WireEntry] = &[ method: "account_sign_vrf", kind: WireKind::Request(ACCOUNT_SIGN_VRF), }, + WireEntry { + method: "chain_get_supported_chains", + kind: WireKind::Request(CHAIN_GET_SUPPORTED_CHAINS), + }, + WireEntry { + method: "chain_resolve_chain", + kind: WireKind::Request(CHAIN_RESOLVE_CHAIN), + }, ]; diff --git a/rust/crates/truapi/src/api/chain.rs b/rust/crates/truapi/src/api/chain.rs index a37a7cf14..01ec15948 100644 --- a/rust/crates/truapi/src/api/chain.rs +++ b/rust/crates/truapi/src/api/chain.rs @@ -9,14 +9,16 @@ use crate::versioned::chain::{ RemoteChainHeadStopOperationRequest, RemoteChainHeadStopOperationResponse, RemoteChainHeadStorageError, RemoteChainHeadStorageRequest, RemoteChainHeadStorageResponse, RemoteChainHeadUnpinError, RemoteChainHeadUnpinRequest, RemoteChainHeadUnpinResponse, + RemoteChainResolveChainError, RemoteChainResolveChainRequest, RemoteChainResolveChainResponse, RemoteChainSpecChainNameError, RemoteChainSpecChainNameRequest, RemoteChainSpecChainNameResponse, RemoteChainSpecGenesisHashError, RemoteChainSpecGenesisHashRequest, RemoteChainSpecGenesisHashResponse, RemoteChainSpecPropertiesError, RemoteChainSpecPropertiesRequest, - RemoteChainSpecPropertiesResponse, RemoteChainTransactionBroadcastError, - RemoteChainTransactionBroadcastRequest, RemoteChainTransactionBroadcastResponse, - RemoteChainTransactionStopError, RemoteChainTransactionStopRequest, - RemoteChainTransactionStopResponse, + RemoteChainSpecPropertiesResponse, RemoteChainSupportedChainsError, + RemoteChainSupportedChainsRequest, RemoteChainSupportedChainsResponse, + RemoteChainTransactionBroadcastError, RemoteChainTransactionBroadcastRequest, + RemoteChainTransactionBroadcastResponse, RemoteChainTransactionStopError, + RemoteChainTransactionStopRequest, RemoteChainTransactionStopResponse, }; use crate::wire; use crate::{CallContext, CallError, Subscription}; @@ -389,4 +391,40 @@ pub trait Chain: Send + Sync { { Err(CallError::unavailable()) } + + /// Enumerate the chains this host serves (RFC 0026). + /// + /// ```ts + /// const result = await truapi.chain.getSupportedChains(); + /// assert(result.isOk(), "getSupportedChains failed:", result); + /// console.log("supported chains:", result.value.chains); + /// ``` + #[wire(request_id = 166)] + async fn get_supported_chains( + &self, + _cx: &CallContext, + _request: RemoteChainSupportedChainsRequest, + ) -> Result> + { + Err(CallError::unavailable()) + } + + /// Resolve a (name, network) pair to the chain's genesis hash (RFC 0026). + /// + /// ```ts + /// const result = await truapi.chain.resolveChain({ + /// name: "asset-hub", + /// network: "paseo", + /// }); + /// assert(result.isOk(), "resolveChain failed:", result); + /// console.log("genesis hash:", result.value.genesisHash); + /// ``` + #[wire(request_id = 168)] + async fn resolve_chain( + &self, + _cx: &CallContext, + _request: RemoteChainResolveChainRequest, + ) -> Result> { + Err(CallError::unavailable()) + } } diff --git a/rust/crates/truapi/src/v01/chain.rs b/rust/crates/truapi/src/v01/chain.rs index f8dd524b0..6517749c5 100644 --- a/rust/crates/truapi/src/v01/chain.rs +++ b/rust/crates/truapi/src/v01/chain.rs @@ -1,5 +1,7 @@ use parity_scale_codec::{Decode, Encode}; +use super::common::GenericError; + /// One entry of a runtime's supported API list. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct RuntimeApi { @@ -353,3 +355,46 @@ pub struct RemoteChainTransactionBroadcastResponse { /// Broadcast operation identifier, if available. pub operation_id: Option, } + +/// One chain a host serves. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct HostChainDescriptor { + /// Stable machine key for the chain's role, e.g. "asset-hub". + pub name: String, + /// Ecosystem the chain belongs to, e.g. "polkadot", "kusama", "paseo". + pub network: String, + /// Genesis hash identifying the chain in all chain-scoped calls. + pub genesis_hash: Vec, +} + +/// Response listing every chain the host serves. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct RemoteChainSupportedChainsResponse { + /// Complete set of chains available through this host. + pub chains: Vec, +} + +/// Request to resolve a named chain within a network. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct RemoteChainResolveChainRequest { + /// Stable machine key, e.g. "asset-hub". + pub name: String, + /// Ecosystem string, e.g. "polkadot", "paseo". + pub network: String, +} + +/// Response carrying the resolved genesis hash. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct RemoteChainResolveChainResponse { + /// Genesis hash of the resolved chain. + pub genesis_hash: Vec, +} + +/// Error from [`crate::api::Chain::resolve_chain`]. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum RemoteChainResolveChainError { + /// No supported chain matches the requested (name, network) pair. + NotFound, + /// Catch-all. + Unknown(GenericError), +} diff --git a/rust/crates/truapi/src/versioned/chain.rs b/rust/crates/truapi/src/versioned/chain.rs index b8a2ccb32..b5342fe8b 100644 --- a/rust/crates/truapi/src/versioned/chain.rs +++ b/rust/crates/truapi/src/versioned/chain.rs @@ -41,4 +41,10 @@ truapi_macros::versioned_type! { pub enum RemoteChainTransactionStopRequest { V1 => v01::RemoteChainTransactionStopRequest } pub enum RemoteChainTransactionStopResponse { V1 } pub enum RemoteChainTransactionStopError { V1 => v01::GenericError } + pub enum RemoteChainSupportedChainsRequest { V1 } + pub enum RemoteChainSupportedChainsResponse { V1 => v01::RemoteChainSupportedChainsResponse } + pub enum RemoteChainSupportedChainsError { V1 => v01::GenericError } + pub enum RemoteChainResolveChainRequest { V1 => v01::RemoteChainResolveChainRequest } + pub enum RemoteChainResolveChainResponse { V1 => v01::RemoteChainResolveChainResponse } + pub enum RemoteChainResolveChainError { V1 => v01::RemoteChainResolveChainError } } From d679adbd7d400c2608aba08ca1b987f0e645854f Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Thu, 6 Aug 2026 14:51:17 -0300 Subject: [PATCH 2/8] resolve by name only and move network on response --- docs/rfcs/0026-supported-chains.md | 33 ++++++++++++++++------------- rust/crates/truapi/src/api/chain.rs | 4 ++-- rust/crates/truapi/src/v01/chain.rs | 10 ++++----- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/docs/rfcs/0026-supported-chains.md b/docs/rfcs/0026-supported-chains.md index 742265d97..fb321943d 100644 --- a/docs/rfcs/0026-supported-chains.md +++ b/docs/rfcs/0026-supported-chains.md @@ -14,7 +14,7 @@ owner: "@valentinfernandez1" ## Summary -Add two methods to the `Chain` trait. `get_supported_chains` returns the complete set of chains the host will serve, each as a descriptor carrying a stable machine name (for example `"asset-hub"`), an ecosystem network string (for example `"paseo"`), and the chain's genesis hash. `resolve_chain` maps one `(name, network)` pair to its genesis hash, or fails with `NotFound`. Both are answered in-core from a single new platform syscall, so each host implements exactly one callback over configuration it already has. +Add two methods to the `Chain` trait. `get_supported_chains` returns the ecosystem the host is configured for (for example `"paseo"`) and the complete set of chains it will serve, each as a descriptor carrying a stable machine name (for example `"asset-hub"`) and the chain's genesis hash. `resolve_chain` maps one name to its genesis hash, resolved against that same environment, or fails with `NotFound`. Both are answered in-core from a single new platform syscall, so each host implements exactly one callback over configuration it already has. ## Motivation @@ -38,6 +38,7 @@ The fix is to make the host's chain set discoverable over the wire. Hosts alread /// ```ts /// const result = await truapi.chain.getSupportedChains(); /// assert(result.isOk(), "getSupportedChains failed:", result); +/// console.log("network:", result.value.network); /// console.log("supported chains:", result.value.chains); /// ``` #[wire(request_id = 166)] @@ -55,6 +56,8 @@ The request carries no payload (a payload-less `V1` envelope on the wire, a no-a ```rust /// Response listing every chain the host serves. struct RemoteChainSupportedChainsResponse { + /// Ecosystem the host is configured for, e.g. "polkadot", "kusama", "paseo". + network: String, /// Complete set of chains available through this host. chains: Vec, } @@ -63,24 +66,23 @@ struct RemoteChainSupportedChainsResponse { struct HostChainDescriptor { /// Stable machine key for the chain's role, e.g. "asset-hub". name: String, - /// Ecosystem the chain belongs to, e.g. "polkadot", "kusama", "paseo", "devnet". - network: String, /// Genesis hash identifying the chain in all chain-scoped calls. genesis_hash: Vec, } ``` +A host serves exactly one environment, so `network` appears once on the response rather than repeated per entry. A response looks like `{ network: "paseo", chains: [{ name: "asset-hub", genesisHash: "0xbf04..." }, { name: "bulletin", genesisHash: "0x..." }, ...] }`. + The error is the plain `GenericError` catch-all, matching the neighboring `getSpec*` methods. ### `chain.resolveChain` ```rust -/// Resolve a (name, network) pair to the chain's genesis hash. +/// Resolve a chain name to its genesis hash. /// /// ```ts /// const result = await truapi.chain.resolveChain({ /// name: "asset-hub", -/// network: "paseo", /// }); /// assert(result.isOk(), "resolveChain failed:", result); /// console.log("genesis hash:", result.value.genesisHash); @@ -96,12 +98,10 @@ async fn resolve_chain( ``` ```rust -/// Request to resolve a named chain within a network. +/// Request to resolve a named chain. struct RemoteChainResolveChainRequest { /// Stable machine key, e.g. "asset-hub". name: String, - /// Ecosystem string, e.g. "polkadot", "paseo". - network: String, } /// Response carrying the resolved genesis hash. @@ -112,23 +112,25 @@ struct RemoteChainResolveChainResponse { /// Error from resolve_chain. enum RemoteChainResolveChainError { - /// No supported chain matches the requested (name, network) pair. + /// No supported chain matches the requested name. NotFound, /// Catch-all. Unknown(GenericError), } ``` +The request deliberately carries no network selector. A product does not get to choose which network it operates on; the host is configured for exactly one environment (polkadot in production), and `resolve_chain` resolves the name against that. Asking the product to name the environment would reintroduce the guessing this RFC removes. + Both methods land in `v01` (the single unfrozen wire) beside the existing chain-metadata methods `getSpecGenesisHash` (94), `getSpecChainName` (96), and `getSpecProperties` (98), taking the next free wire ids, and are re-exported through `truapi::latest`. ### Semantics and invariants - **Completeness.** The returned list is the complete set of chains the host will serve `chain.*` and `signing.*` calls for. A genesis hash absent from the list will not be served, and every listed hash will be. -- **Uniqueness.** `(name, network)` is unique within one host's response, so `resolve_chain` is a plain lookup with a single answer. +- **Uniqueness.** `name` is unique within one host's response, so `resolve_chain` is a plain lookup with a single answer. - **Name stability.** Names are stable machine keys across sessions. A product that persisted `"asset-hub"` resolves it again after any wipe and receives the current genesis hash. - **Fixed per connection.** The list does not change for the lifetime of a connection. There is no subscription; a product observes host-side changes by reconnecting. -`network` is an open ecosystem string, not a `Mainnet`/`Testnet` enum, because a binary flag cannot distinguish two testnets: a host serving both a Paseo asset hub and a devnet asset hub needs `("asset-hub", "paseo")` and `("asset-hub", "devnet")` to be different keys. +`network` appears only on the discovery response and is informational, not a selector: it tells a product or SDK which environment the host is running, so tooling can derive the environment from the host instead of asking the developer to configure it. It is an open ecosystem string ("polkadot", "kusama", "paseo", "devnet"), not a `Mainnet`/`Testnet` enum, because a binary flag cannot distinguish two testnets. No host serves more than one environment at a time; if one ever does, it disambiguates in its name strings (`"paseo-asset-hub"`), which the open registry already permits with no wire change. Descriptors deliberately exclude display names and token properties. Once a product holds the genesis hash, that metadata is already reachable through `getSpecChainName` and `getSpecProperties`. @@ -152,8 +154,8 @@ The product never embeds a hash. After a testnet wipe the host updates its confi The core does not own the chain set. `system.featureSupported(Chain { genesis_hash })` is already a thin shim in `rust/crates/truapi-server/src/host_logic/features.rs` delegating to `truapi_platform::Features`, and `ChainProvider::connect(genesis_hash)` opens JSON-RPC pipes on demand. This RFC follows the same delegation pattern: -- `truapi-platform` gains one syscall on `Features`, shaped like `supported_chains() -> Result, GenericError>`. -- `truapi-server` answers **both** wire methods in-core from that single syscall: `get_supported_chains` returns the list as-is, and `resolve_chain` filters it by `(name, network)`, mapping a miss to `NotFound`. +- `truapi-platform` gains one syscall on `Features`, shaped like `supported_chains() -> Result` (the host's network plus its chain descriptors). +- `truapi-server` answers **both** wire methods in-core from that single syscall: `get_supported_chains` returns the list as-is, and `resolve_chain` looks up the name in it, mapping a miss to `NotFound`. Hosts therefore implement exactly one callback, backed by configuration they already maintain. dotli's per-environment named slots (`relay`, `assethub`, `bulletin`, `people`, each with a genesis hash) map directly onto descriptors; the iOS `TrUAPIHost` and the host CLI expose their equivalent config the same way. Because `resolve_chain` is answered in-core over the same data, the completeness invariant holds by construction: the two methods cannot disagree. @@ -166,10 +168,11 @@ The change is purely additive: two new methods with fresh wire ids, no changes t ## Drawbacks -- Name and network strings are minted by host configuration, not by the protocol. Two hosts could use different names for the same chain until a spec-level registry exists (see Unresolved Questions). +- Name and network strings are minted by host configuration, not by the protocol. Two hosts could use different names for the same chain until a spec-level registry exists, which can follow as a separate RFC. - No change notification. A host that reconfigures mid-session cannot inform connected products; they observe the change only on reconnect. This keeps the API subscription-free and matches how host config changes actually roll out (host restarts). ## Alternatives -- **Take chain names instead of `genesisHash` in every chain-scoped call.** Was discartes as this is a breaking change across the Rust trait, codegen, the TS client, dotli, the iOS host, and the product SDK. The genesis hash also remains necessary internally, since connections are keyed by it and signed payloads embed it via `CheckGenesis`. +- **Take chain names instead of `genesisHash` in every chain-scoped call.** This was discarded as it is a breaking change across the Rust trait, codegen, the TS client, dotli, the iOS host, and the product SDK. The genesis hash also remains necessary internally, since connections are keyed by it and signed payloads embed it via `CheckGenesis`. - **A protocol-defined closed enum of chains.** This was discarded as adding a chain would require a protocol release, which is exactly the coupling this RFC removes. +- **A `network` selector on `resolve_chain`.** This was discarded because the product does not choose its network, the host's configuration does. Asking for `(name, network)` would make products encode the environment again, which is the hard-coding this RFC removes. diff --git a/rust/crates/truapi/src/api/chain.rs b/rust/crates/truapi/src/api/chain.rs index 01ec15948..e9368f50e 100644 --- a/rust/crates/truapi/src/api/chain.rs +++ b/rust/crates/truapi/src/api/chain.rs @@ -397,6 +397,7 @@ pub trait Chain: Send + Sync { /// ```ts /// const result = await truapi.chain.getSupportedChains(); /// assert(result.isOk(), "getSupportedChains failed:", result); + /// console.log("network:", result.value.network); /// console.log("supported chains:", result.value.chains); /// ``` #[wire(request_id = 166)] @@ -409,12 +410,11 @@ pub trait Chain: Send + Sync { Err(CallError::unavailable()) } - /// Resolve a (name, network) pair to the chain's genesis hash (RFC 0026). + /// Resolve a chain name to its genesis hash (RFC 0026). /// /// ```ts /// const result = await truapi.chain.resolveChain({ /// name: "asset-hub", - /// network: "paseo", /// }); /// assert(result.isOk(), "resolveChain failed:", result); /// console.log("genesis hash:", result.value.genesisHash); diff --git a/rust/crates/truapi/src/v01/chain.rs b/rust/crates/truapi/src/v01/chain.rs index 6517749c5..f2d7c57f9 100644 --- a/rust/crates/truapi/src/v01/chain.rs +++ b/rust/crates/truapi/src/v01/chain.rs @@ -361,8 +361,6 @@ pub struct RemoteChainTransactionBroadcastResponse { pub struct HostChainDescriptor { /// Stable machine key for the chain's role, e.g. "asset-hub". pub name: String, - /// Ecosystem the chain belongs to, e.g. "polkadot", "kusama", "paseo". - pub network: String, /// Genesis hash identifying the chain in all chain-scoped calls. pub genesis_hash: Vec, } @@ -370,17 +368,17 @@ pub struct HostChainDescriptor { /// Response listing every chain the host serves. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct RemoteChainSupportedChainsResponse { + /// Ecosystem the host is configured for, e.g. "polkadot", "kusama", "paseo". + pub network: String, /// Complete set of chains available through this host. pub chains: Vec, } -/// Request to resolve a named chain within a network. +/// Request to resolve a named chain against the host's configured environment. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct RemoteChainResolveChainRequest { /// Stable machine key, e.g. "asset-hub". pub name: String, - /// Ecosystem string, e.g. "polkadot", "paseo". - pub network: String, } /// Response carrying the resolved genesis hash. @@ -393,7 +391,7 @@ pub struct RemoteChainResolveChainResponse { /// Error from [`crate::api::Chain::resolve_chain`]. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub enum RemoteChainResolveChainError { - /// No supported chain matches the requested (name, network) pair. + /// No supported chain matches the requested name. NotFound, /// Catch-all. Unknown(GenericError), From 8b69ed2e8dfd951f080d5e8c4527b9873fcf9f3b Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Fri, 7 Aug 2026 07:49:47 -0300 Subject: [PATCH 3/8] replace the method pair with a batch getChainInfo keyed by a role enum --- docs/rfcs/0026-supported-chains.md | 148 ++++++++---------- .../truapi-codegen/tests/golden/dispatcher.rs | 38 +---- .../truapi-codegen/tests/golden/wire_table.rs | 18 +-- .../truapi-server/src/generated/dispatcher.rs | 44 +----- .../truapi-server/src/generated/wire_table.rs | 18 +-- rust/crates/truapi/src/api/account.rs | 12 +- rust/crates/truapi/src/api/chain.rs | 143 +++++++++-------- rust/crates/truapi/src/api/signing.rs | 24 ++- rust/crates/truapi/src/api/system.rs | 6 +- rust/crates/truapi/src/v01/chain.rs | 59 ++++--- rust/crates/truapi/src/versioned/chain.rs | 9 +- 11 files changed, 227 insertions(+), 292 deletions(-) diff --git a/docs/rfcs/0026-supported-chains.md b/docs/rfcs/0026-supported-chains.md index fb321943d..758c27e91 100644 --- a/docs/rfcs/0026-supported-chains.md +++ b/docs/rfcs/0026-supported-chains.md @@ -9,12 +9,12 @@ owner: "@valentinfernandez1" | --------------- | -------------------------------------------------------------------------------------------------- | | **RFC Number** | 26 | | **Start Date** | 2026-08-06 | -| **Description** | Two `Chain` methods letting products enumerate the chains a host serves and resolve stable names to genesis hashes. | +| **Description** | A `Chain` method resolving protocol-defined chain identifiers to genesis hashes against the host's environment. | | **Authors** | Valentin Fernandez | ## Summary -Add two methods to the `Chain` trait. `get_supported_chains` returns the ecosystem the host is configured for (for example `"paseo"`) and the complete set of chains it will serve, each as a descriptor carrying a stable machine name (for example `"asset-hub"`) and the chain's genesis hash. `resolve_chain` maps one name to its genesis hash, resolved against that same environment, or fails with `NotFound`. Both are answered in-core from a single new platform syscall, so each host implements exactly one callback over configuration it already has. +Add one method to the `Chain` trait. `get_chain_info` takes the chain identifiers a product wants to use, drawn from a closed role enum (`Relay`, `AssetHub`, `People`, `Bulletin`), and returns the ecosystem the host is configured for (for example `"paseo"`) plus one `ChainInfo` (name and genesis hash) per requested identifier, resolved against that environment. It is answered in-core from a single new platform syscall, so each host implements exactly one callback over configuration it already has. ## Motivation @@ -30,122 +30,103 @@ The fix is to make the host's chain set discoverable over the wire. Hosts alread ## Detailed Design -### `chain.getSupportedChains` +### `chain.getChainInfo` ```rust -/// Enumerate the chains this host serves. +/// Resolve chain identifiers to genesis hashes against the host's +/// configured environment. /// /// ```ts -/// const result = await truapi.chain.getSupportedChains(); -/// assert(result.isOk(), "getSupportedChains failed:", result); +/// const result = await truapi.chain.getChainInfo({ +/// chains: ["AssetHub"], +/// }); +/// assert(result.isOk(), "getChainInfo failed:", result); /// console.log("network:", result.value.network); -/// console.log("supported chains:", result.value.chains); +/// console.log("asset hub genesis:", result.value.chains[0].genesisHash); /// ``` #[wire(request_id = 166)] -async fn get_supported_chains( +async fn get_chain_info( &self, _cx: &CallContext, - _request: RemoteChainSupportedChainsRequest, -) -> Result> { + _request: RemoteChainInfoRequest, +) -> Result> { Err(CallError::unavailable()) } ``` -The request carries no payload (a payload-less `V1` envelope on the wire, a no-argument call in the TS client). The response: - ```rust -/// Response listing every chain the host serves. -struct RemoteChainSupportedChainsResponse { - /// Ecosystem the host is configured for, e.g. "polkadot", "kusama", "paseo". - network: String, - /// Complete set of chains available through this host. - chains: Vec, +/// Role of a chain within the host's configured environment. +enum ChainIdentifier { + /// The relay chain. + Relay, + /// The asset hub system chain. + AssetHub, + /// The people chain. + People, + /// The bulletin chain. + Bulletin, } -/// One chain a host serves. -struct HostChainDescriptor { - /// Stable machine key for the chain's role, e.g. "asset-hub". +/// Resolved chain data for one requested ChainIdentifier. +struct ChainInfo { + /// Host-assigned chain name, e.g. "asset-hub". name: String, /// Genesis hash identifying the chain in all chain-scoped calls. - genesis_hash: Vec, + genesis_hash: [u8; 32], } -``` - -A host serves exactly one environment, so `network` appears once on the response rather than repeated per entry. A response looks like `{ network: "paseo", chains: [{ name: "asset-hub", genesisHash: "0xbf04..." }, { name: "bulletin", genesisHash: "0x..." }, ...] }`. -The error is the plain `GenericError` catch-all, matching the neighboring `getSpec*` methods. - -### `chain.resolveChain` - -```rust -/// Resolve a chain name to its genesis hash. -/// -/// ```ts -/// const result = await truapi.chain.resolveChain({ -/// name: "asset-hub", -/// }); -/// assert(result.isOk(), "resolveChain failed:", result); -/// console.log("genesis hash:", result.value.genesisHash); -/// ``` -#[wire(request_id = 168)] -async fn resolve_chain( - &self, - _cx: &CallContext, - _request: RemoteChainResolveChainRequest, -) -> Result> { - Err(CallError::unavailable()) +/// Request to resolve chain identifiers against the host's environment. +struct RemoteChainInfoRequest { + /// Chains to resolve. + chains: Vec, } -``` -```rust -/// Request to resolve a named chain. -struct RemoteChainResolveChainRequest { - /// Stable machine key, e.g. "asset-hub". - name: String, -} - -/// Response carrying the resolved genesis hash. -struct RemoteChainResolveChainResponse { - /// Genesis hash of the resolved chain. - genesis_hash: Vec, +/// Response carrying one ChainInfo per requested identifier, in request order. +struct RemoteChainInfoResponse { + /// Ecosystem the host is configured for, e.g. "polkadot", "kusama", "paseo". + network: String, + /// Resolved chains, aligned with the request's `chains`. + chains: Vec, } -/// Error from resolve_chain. -enum RemoteChainResolveChainError { - /// No supported chain matches the requested name. - NotFound, +/// Error from get_chain_info. +enum RemoteChainInfoError { + /// The host does not serve one of the requested chains. + NotSupported { + /// First requested identifier the host does not serve. + chain: ChainIdentifier, + }, /// Catch-all. Unknown(GenericError), } ``` -The request deliberately carries no network selector. A product does not get to choose which network it operates on; the host is configured for exactly one environment (polkadot in production), and `resolve_chain` resolves the name against that. Asking the product to name the environment would reintroduce the guessing this RFC removes. +The request is a batch: a product names every chain it needs in one call and gets them all back in one round trip. `ChainIdentifier` is a closed protocol enum of chain roles, not chain instances; the host maps each role to the concrete chain of its configured environment. Adding a new role is an additive enum variant. -Both methods land in `v01` (the single unfrozen wire) beside the existing chain-metadata methods `getSpecGenesisHash` (94), `getSpecChainName` (96), and `getSpecProperties` (98), taking the next free wire ids, and are re-exported through `truapi::latest`. +The request deliberately carries no network selector. A product does not get to choose which network it operates on; the host is configured for exactly one environment (polkadot in production), and every identifier resolves against that. Asking the product to name the environment would reintroduce the guessing this RFC removes. ### Semantics and invariants -- **Completeness.** The returned list is the complete set of chains the host will serve `chain.*` and `signing.*` calls for. A genesis hash absent from the list will not be served, and every listed hash will be. -- **Uniqueness.** `name` is unique within one host's response, so `resolve_chain` is a plain lookup with a single answer. -- **Name stability.** Names are stable machine keys across sessions. A product that persisted `"asset-hub"` resolves it again after any wipe and receives the current genesis hash. -- **Fixed per connection.** The list does not change for the lifetime of a connection. There is no subscription; a product observes host-side changes by reconnecting. +- **Serviceability.** Every genesis hash returned by `get_chain_info` is a chain the host will serve `chain.*` and `signing.*` calls for. A `NotSupported` identifier will not be served. +- **Alignment.** The response's `chains` has exactly one entry per requested identifier, in request order, so products index it positionally. +- **All or nothing.** If any requested identifier is not served, the whole call fails with `NotSupported` naming the first such identifier; there are no partial responses. +- **Stability.** An identifier resolves to the same chain for the lifetime of a connection. There is no subscription; a product observes host-side changes (such as a testnet wipe) by reconnecting. -`network` appears only on the discovery response and is informational, not a selector: it tells a product or SDK which environment the host is running, so tooling can derive the environment from the host instead of asking the developer to configure it. It is an open ecosystem string ("polkadot", "kusama", "paseo", "devnet"), not a `Mainnet`/`Testnet` enum, because a binary flag cannot distinguish two testnets. No host serves more than one environment at a time; if one ever does, it disambiguates in its name strings (`"paseo-asset-hub"`), which the open registry already permits with no wire change. +`network` is informational, not a selector: it tells a product or SDK which environment the host is running, so tooling can derive the environment from the host instead of asking the developer to configure it. It is an open ecosystem string ("polkadot", "kusama", "paseo", "devnet"), not a `Mainnet`/`Testnet` enum, because a binary flag cannot distinguish two testnets. -Descriptors deliberately exclude display names and token properties. Once a product holds the genesis hash, that metadata is already reachable through `getSpecChainName` and `getSpecProperties`. +`ChainInfo` deliberately excludes display names and token properties. Once a product holds the genesis hash, that metadata is already reachable through `getSpecChainName` and `getSpecProperties`. ### Typical product flow ```ts -const supported = await truapi.chain.getSupportedChains(); -assert(supported.isOk(), "getSupportedChains failed:", supported); +const info = await truapi.chain.getChainInfo({ chains: ["AssetHub", "People"] }); +assert(info.isOk(), "getChainInfo failed:", info); -const hub = supported.value.chains.find((c) => c.name === "asset-hub"); -assert(hub !== undefined, "host serves no asset hub"); +const [assetHub, people] = info.value.chains; -const name = await truapi.chain.getSpecChainName({ genesisHash: hub.genesisHash }); +const name = await truapi.chain.getSpecChainName({ genesisHash: assetHub.genesisHash }); assert(name.isOk(), "getSpecChainName failed:", name); -console.log("connected to:", name.value.chainName); +console.log(`connected to ${name.value.chainName} on ${info.value.network}`); ``` The product never embeds a hash. After a testnet wipe the host updates its config, the product reconnects, and the same code path picks up the new hash. @@ -154,12 +135,12 @@ The product never embeds a hash. After a testnet wipe the host updates its confi The core does not own the chain set. `system.featureSupported(Chain { genesis_hash })` is already a thin shim in `rust/crates/truapi-server/src/host_logic/features.rs` delegating to `truapi_platform::Features`, and `ChainProvider::connect(genesis_hash)` opens JSON-RPC pipes on demand. This RFC follows the same delegation pattern: -- `truapi-platform` gains one syscall on `Features`, shaped like `supported_chains() -> Result` (the host's network plus its chain descriptors). -- `truapi-server` answers **both** wire methods in-core from that single syscall: `get_supported_chains` returns the list as-is, and `resolve_chain` looks up the name in it, mapping a miss to `NotFound`. +- `truapi-platform` gains one syscall on `Features` returning the host's network string and its full identifier-to-chain mapping. +- `truapi-server` answers `get_chain_info` in-core from that syscall, resolving each requested identifier and mapping the first miss to `NotSupported`. -Hosts therefore implement exactly one callback, backed by configuration they already maintain. dotli's per-environment named slots (`relay`, `assethub`, `bulletin`, `people`, each with a genesis hash) map directly onto descriptors; the iOS `TrUAPIHost` and the host CLI expose their equivalent config the same way. Because `resolve_chain` is answered in-core over the same data, the completeness invariant holds by construction: the two methods cannot disagree. +Hosts therefore implement exactly one callback, backed by configuration they already maintain. dotli's per-environment named slots (`relay`, `assethub`, `bulletin`, `people`, each with a genesis hash) map one-to-one onto `ChainIdentifier` variants; the iOS `TrUAPIHost` and the host CLI expose their equivalent config the same way. -The change is purely additive: two new methods with fresh wire ids, no changes to existing calls or types. Existing products keep working unchanged, including their hard-coded constants, and can migrate to discovery at their own pace. +The change is purely additive: one new method with a fresh wire id, no changes to existing calls or types. Existing products keep working unchanged, including their hard-coded constants, and can migrate at their own pace. ## Non-goals @@ -168,11 +149,12 @@ The change is purely additive: two new methods with fresh wire ids, no changes t ## Drawbacks -- Name and network strings are minted by host configuration, not by the protocol. Two hosts could use different names for the same chain until a spec-level registry exists, which can follow as a separate RFC. +- Adding a new chain role requires a protocol release (an additive `ChainIdentifier` variant) and host support for it. The closed enum trades that coupling for typo-proof, host-portable identifiers. - No change notification. A host that reconfigures mid-session cannot inform connected products; they observe the change only on reconnect. This keeps the API subscription-free and matches how host config changes actually roll out (host restarts). ## Alternatives - **Take chain names instead of `genesisHash` in every chain-scoped call.** This was discarded as it is a breaking change across the Rust trait, codegen, the TS client, dotli, the iOS host, and the product SDK. The genesis hash also remains necessary internally, since connections are keyed by it and signed payloads embed it via `CheckGenesis`. -- **A protocol-defined closed enum of chains.** This was discarded as adding a chain would require a protocol release, which is exactly the coupling this RFC removes. -- **A `network` selector on `resolve_chain`.** This was discarded because the product does not choose its network, the host's configuration does. Asking for `(name, network)` would make products encode the environment again, which is the hard-coding this RFC removes. +- **Separate discovery and lookup methods (`getSupportedChains` + `resolveChain`).** This was discarded during review: a product that needs one chain should not fetch and filter the host's full mapping, and the batch request already covers the multi-chain case in one round trip. +- **Free-form string identifiers.** This was discarded because names minted by host configuration form a de facto registry with no governance: two hosts could name the same chain differently, and typos fail only at runtime. The closed role enum is typo-proof, identical across hosts, and versioned with the protocol. +- **A `network` selector on the request.** This was discarded because the product does not choose its network, the host's configuration does. Asking the product to name the environment would make it encode the environment again, which is the hard-coding this RFC removes. diff --git a/rust/crates/truapi-codegen/tests/golden/dispatcher.rs b/rust/crates/truapi-codegen/tests/golden/dispatcher.rs index 92341c319..a65471f8a 100644 --- a/rust/crates/truapi-codegen/tests/golden/dispatcher.rs +++ b/rust/crates/truapi-codegen/tests/golden/dispatcher.rs @@ -625,53 +625,25 @@ where }) }); } - { - let host = host.clone(); - dispatcher.on_request(wire_table::CHAIN_GET_SUPPORTED_CHAINS, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::chain::RemoteChainSupportedChainsRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id.clone()); - let response: versioned::chain::RemoteChainSupportedChainsResponse = match host.get_supported_chains(&cx, request).await { - Ok(value) => value, - Err(err) => { - return Ok(encode_versioned_err_payload(err, target_version)); - } - }; - Ok(encode_versioned_ok_payload(response)) - }) - }); - } { let host = host; - dispatcher.on_request(wire_table::CHAIN_RESOLVE_CHAIN, move |request_id: String, bytes: Vec| { + dispatcher.on_request(wire_table::CHAIN_GET_CHAIN_INFO, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainResolveChainRequest = match Decode::decode(&mut &bytes[..]) { + let request: versioned::chain::RemoteChainInfoRequest = match Decode::decode(&mut &bytes[..]) { Ok(request) => request, Err(err) => { - let error: truapi::CallError = + let error: truapi::CallError = truapi::CallError::MalformedFrame { reason: err.to_string() }; return Ok(encode_versioned_err_payload( error, - ::LATEST, + ::LATEST, )); } }; let target_version = request.version(); let cx = CallContext::with_request_id(request_id.clone()); - let response: versioned::chain::RemoteChainResolveChainResponse = match host.resolve_chain(&cx, request).await { + let response: versioned::chain::RemoteChainInfoResponse = match host.get_chain_info(&cx, request).await { Ok(value) => value, Err(err) => { return Ok(encode_versioned_err_payload(err, target_version)); diff --git a/rust/crates/truapi-codegen/tests/golden/wire_table.rs b/rust/crates/truapi-codegen/tests/golden/wire_table.rs index 0aa3a9074..5c32c3c10 100644 --- a/rust/crates/truapi-codegen/tests/golden/wire_table.rs +++ b/rust/crates/truapi-codegen/tests/golden/wire_table.rs @@ -466,18 +466,12 @@ pub const ACCOUNT_SIGN_VRF: RequestFrameIds = RequestFrameIds { response_id: 165, }; -/// Wire discriminants for `chain_get_supported_chains`. -pub const CHAIN_GET_SUPPORTED_CHAINS: RequestFrameIds = RequestFrameIds { +/// Wire discriminants for `chain_get_chain_info`. +pub const CHAIN_GET_CHAIN_INFO: RequestFrameIds = RequestFrameIds { request_id: 166, response_id: 167, }; -/// Wire discriminants for `chain_resolve_chain`. -pub const CHAIN_RESOLVE_CHAIN: RequestFrameIds = RequestFrameIds { - request_id: 168, - response_id: 169, -}; - /// 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] = &[ @@ -742,11 +736,7 @@ pub const WIRE_TABLE: &[WireEntry] = &[ kind: WireKind::Request(ACCOUNT_SIGN_VRF), }, WireEntry { - method: "chain_get_supported_chains", - kind: WireKind::Request(CHAIN_GET_SUPPORTED_CHAINS), - }, - WireEntry { - method: "chain_resolve_chain", - kind: WireKind::Request(CHAIN_RESOLVE_CHAIN), + method: "chain_get_chain_info", + kind: WireKind::Request(CHAIN_GET_CHAIN_INFO), }, ]; diff --git a/rust/crates/truapi-server/src/generated/dispatcher.rs b/rust/crates/truapi-server/src/generated/dispatcher.rs index 4a7cba817..fa1b9a0f8 100644 --- a/rust/crates/truapi-server/src/generated/dispatcher.rs +++ b/rust/crates/truapi-server/src/generated/dispatcher.rs @@ -745,60 +745,32 @@ where }) }); } - { - let host = host.clone(); - dispatcher.on_request(wire_table::CHAIN_GET_SUPPORTED_CHAINS, move |request_id: String, bytes: Vec| { - let host = host.clone(); - Box::pin(async move { - let request: versioned::chain::RemoteChainSupportedChainsRequest = match Decode::decode(&mut &bytes[..]) { - Ok(request) => request, - Err(err) => { - let error: truapi::CallError = - truapi::CallError::MalformedFrame { reason: err.to_string() }; - return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); - } - }; - let target_version = request.version(); - let cx = CallContext::with_request_id(request_id.clone()); - let response: versioned::chain::RemoteChainSupportedChainsResponse = match host.get_supported_chains(&cx, request).await { - Ok(value) => value, - Err(err) => { - return Ok(encode_versioned_err_payload(err, target_version)); - } - }; - Ok(encode_versioned_ok_payload(response)) - }) - }); - } { let host = host; dispatcher.on_request( - wire_table::CHAIN_RESOLVE_CHAIN, + wire_table::CHAIN_GET_CHAIN_INFO, move |request_id: String, bytes: Vec| { let host = host.clone(); Box::pin(async move { - let request: versioned::chain::RemoteChainResolveChainRequest = + let request: versioned::chain::RemoteChainInfoRequest = match Decode::decode(&mut &bytes[..]) { Ok(request) => request, Err(err) => { let error: truapi::CallError< - versioned::chain::RemoteChainResolveChainError, + versioned::chain::RemoteChainInfoError, > = truapi::CallError::MalformedFrame { reason: err.to_string(), }; return Ok(encode_versioned_err_payload( - error, - ::LATEST, - )); + error, + ::LATEST, + )); } }; let target_version = request.version(); let cx = CallContext::with_request_id(request_id.clone()); - let response: versioned::chain::RemoteChainResolveChainResponse = - match host.resolve_chain(&cx, request).await { + let response: versioned::chain::RemoteChainInfoResponse = + match host.get_chain_info(&cx, request).await { Ok(value) => value, Err(err) => { return Ok(encode_versioned_err_payload(err, target_version)); diff --git a/rust/crates/truapi-server/src/generated/wire_table.rs b/rust/crates/truapi-server/src/generated/wire_table.rs index 0aa3a9074..5c32c3c10 100644 --- a/rust/crates/truapi-server/src/generated/wire_table.rs +++ b/rust/crates/truapi-server/src/generated/wire_table.rs @@ -466,18 +466,12 @@ pub const ACCOUNT_SIGN_VRF: RequestFrameIds = RequestFrameIds { response_id: 165, }; -/// Wire discriminants for `chain_get_supported_chains`. -pub const CHAIN_GET_SUPPORTED_CHAINS: RequestFrameIds = RequestFrameIds { +/// Wire discriminants for `chain_get_chain_info`. +pub const CHAIN_GET_CHAIN_INFO: RequestFrameIds = RequestFrameIds { request_id: 166, response_id: 167, }; -/// Wire discriminants for `chain_resolve_chain`. -pub const CHAIN_RESOLVE_CHAIN: RequestFrameIds = RequestFrameIds { - request_id: 168, - response_id: 169, -}; - /// 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] = &[ @@ -742,11 +736,7 @@ pub const WIRE_TABLE: &[WireEntry] = &[ kind: WireKind::Request(ACCOUNT_SIGN_VRF), }, WireEntry { - method: "chain_get_supported_chains", - kind: WireKind::Request(CHAIN_GET_SUPPORTED_CHAINS), - }, - WireEntry { - method: "chain_resolve_chain", - kind: WireKind::Request(CHAIN_RESOLVE_CHAIN), + method: "chain_get_chain_info", + kind: WireKind::Request(CHAIN_GET_CHAIN_INFO), }, ]; diff --git a/rust/crates/truapi/src/api/account.rs b/rust/crates/truapi/src/api/account.rs index dd8a448c2..c9780ab96 100644 --- a/rust/crates/truapi/src/api/account.rs +++ b/rust/crates/truapi/src/api/account.rs @@ -67,7 +67,9 @@ pub trait Account: Send + Sync { /// Retrieve the contextual alias for a context and ring. /// /// ```ts - /// import { PASEO_NEXT_V2_INDIVIDUALITY } from "@parity/truapi"; + /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["People"] }); + /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); + /// const [people] = chainInfo.value.chains; /// /// const PEOPLE_COLLECTION_ID = /// "0x706f703a706f6c6b61646f742e6e6574776f726b2f70656f706c652d6c697465"; @@ -75,7 +77,7 @@ pub trait Account: Send + Sync { /// const result = await truapi.account.getAccountAlias({ /// context: { productId: "truapi-playground.dot", suffix: { tag: "Left", value: 0 } }, /// ringLocation: { - /// chainId: PASEO_NEXT_V2_INDIVIDUALITY.genesis, + /// chainId: people.genesisHash, /// junctions: [ /// { tag: "PalletInstance", value: 67 }, /// { tag: "CollectionId", value: PEOPLE_COLLECTION_ID }, @@ -97,7 +99,9 @@ pub trait Account: Send + Sync { /// Generate a ring VRF proof; the host selects the member key for the ring. /// /// ```ts - /// import { PASEO_NEXT_V2_INDIVIDUALITY } from "@parity/truapi"; + /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["People"] }); + /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); + /// const [people] = chainInfo.value.chains; /// /// const PEOPLE_COLLECTION_ID = /// "0x706f703a706f6c6b61646f742e6e6574776f726b2f70656f706c652d6c697465"; @@ -105,7 +109,7 @@ pub trait Account: Send + Sync { /// const result = await truapi.account.createAccountProof({ /// context: { productId: "truapi-playground.dot", suffix: { tag: "Left", value: 0 } }, /// ringLocation: { - /// chainId: PASEO_NEXT_V2_INDIVIDUALITY.genesis, + /// chainId: people.genesisHash, /// junctions: [ /// { tag: "PalletInstance", value: 67 }, /// { tag: "CollectionId", value: PEOPLE_COLLECTION_ID }, diff --git a/rust/crates/truapi/src/api/chain.rs b/rust/crates/truapi/src/api/chain.rs index e9368f50e..de12a8d7c 100644 --- a/rust/crates/truapi/src/api/chain.rs +++ b/rust/crates/truapi/src/api/chain.rs @@ -9,16 +9,15 @@ use crate::versioned::chain::{ RemoteChainHeadStopOperationRequest, RemoteChainHeadStopOperationResponse, RemoteChainHeadStorageError, RemoteChainHeadStorageRequest, RemoteChainHeadStorageResponse, RemoteChainHeadUnpinError, RemoteChainHeadUnpinRequest, RemoteChainHeadUnpinResponse, - RemoteChainResolveChainError, RemoteChainResolveChainRequest, RemoteChainResolveChainResponse, + RemoteChainInfoError, RemoteChainInfoRequest, RemoteChainInfoResponse, RemoteChainSpecChainNameError, RemoteChainSpecChainNameRequest, RemoteChainSpecChainNameResponse, RemoteChainSpecGenesisHashError, RemoteChainSpecGenesisHashRequest, RemoteChainSpecGenesisHashResponse, RemoteChainSpecPropertiesError, RemoteChainSpecPropertiesRequest, - RemoteChainSpecPropertiesResponse, RemoteChainSupportedChainsError, - RemoteChainSupportedChainsRequest, RemoteChainSupportedChainsResponse, - RemoteChainTransactionBroadcastError, RemoteChainTransactionBroadcastRequest, - RemoteChainTransactionBroadcastResponse, RemoteChainTransactionStopError, - RemoteChainTransactionStopRequest, RemoteChainTransactionStopResponse, + RemoteChainSpecPropertiesResponse, RemoteChainTransactionBroadcastError, + RemoteChainTransactionBroadcastRequest, RemoteChainTransactionBroadcastResponse, + RemoteChainTransactionStopError, RemoteChainTransactionStopRequest, + RemoteChainTransactionStopResponse, }; use crate::wire; use crate::{CallContext, CallError, Subscription}; @@ -29,15 +28,17 @@ pub trait Chain: Send + Sync { /// Follow the chain head and receive block events. /// /// ```ts - /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; - /// /// import { firstValueFrom, from } from "rxjs"; /// + /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); + /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); + /// const [assetHub] = chainInfo.value.chains; + /// /// const item = await firstValueFrom( /// from( /// truapi.chain.followHeadSubscribe({ /// request: { - /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, + /// genesisHash: assetHub.genesisHash, /// withRuntime: false, /// }, /// }), @@ -57,13 +58,15 @@ pub trait Chain: Send + Sync { /// Fetch a block header. /// /// ```ts - /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; - /// /// import { firstValueFrom, mergeMap } from "rxjs"; /// + /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); + /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); + /// const [assetHub] = chainInfo.value.chains; + /// /// const result = await firstValueFrom( /// withChainHeadFollow({ - /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, + /// genesisHash: assetHub.genesisHash, /// }).pipe( /// mergeMap(({ genesisHash, followSubscriptionId, hash }) => /// truapi.chain.getHeadHeader({ genesisHash, followSubscriptionId, hash }), @@ -85,13 +88,15 @@ pub trait Chain: Send + Sync { /// Fetch a block body. /// /// ```ts - /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; - /// /// import { firstValueFrom, mergeMap } from "rxjs"; /// + /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); + /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); + /// const [assetHub] = chainInfo.value.chains; + /// /// const result = await firstValueFrom( /// withChainHeadFollow({ - /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, + /// genesisHash: assetHub.genesisHash, /// }).pipe( /// mergeMap(({ genesisHash, followSubscriptionId, hash }) => /// truapi.chain.getHeadBody({ genesisHash, followSubscriptionId, hash }), @@ -113,13 +118,15 @@ pub trait Chain: Send + Sync { /// Query runtime storage at a specific block. /// /// ```ts - /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; - /// /// import { firstValueFrom, mergeMap } from "rxjs"; /// + /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); + /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); + /// const [assetHub] = chainInfo.value.chains; + /// /// const result = await firstValueFrom( /// withChainHeadFollow({ - /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, + /// genesisHash: assetHub.genesisHash, /// }).pipe( /// mergeMap(({ genesisHash, followSubscriptionId, hash }) => /// truapi.chain.getHeadStorage({ @@ -146,13 +153,15 @@ pub trait Chain: Send + Sync { /// Invoke a runtime call at a specific block. /// /// ```ts - /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; - /// /// import { firstValueFrom, mergeMap } from "rxjs"; /// + /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); + /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); + /// const [assetHub] = chainInfo.value.chains; + /// /// const result = await firstValueFrom( /// withChainHeadFollow({ - /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, + /// genesisHash: assetHub.genesisHash, /// withRuntime: true, /// }).pipe( /// mergeMap(({ genesisHash, followSubscriptionId, hash }) => @@ -181,13 +190,15 @@ pub trait Chain: Send + Sync { /// Release pinned blocks. /// /// ```ts - /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; - /// /// import { firstValueFrom, mergeMap } from "rxjs"; /// + /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); + /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); + /// const [assetHub] = chainInfo.value.chains; + /// /// const result = await firstValueFrom( /// withChainHeadFollow({ - /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, + /// genesisHash: assetHub.genesisHash, /// }).pipe( /// mergeMap(({ genesisHash, followSubscriptionId, hash }) => /// truapi.chain.unpinHead({ @@ -213,13 +224,15 @@ pub trait Chain: Send + Sync { /// Continue a paused chain-head operation. /// /// ```ts - /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; - /// /// import { firstValueFrom, mergeMap } from "rxjs"; /// + /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); + /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); + /// const [assetHub] = chainInfo.value.chains; + /// /// const result = await firstValueFrom( /// withChainHeadFollow({ - /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, + /// genesisHash: assetHub.genesisHash, /// }).pipe( /// mergeMap(({ genesisHash, followSubscriptionId }) => /// truapi.chain.continueHead({ @@ -245,13 +258,15 @@ pub trait Chain: Send + Sync { /// Stop a chain-head operation. /// /// ```ts - /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; - /// /// import { firstValueFrom, mergeMap } from "rxjs"; /// + /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); + /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); + /// const [assetHub] = chainInfo.value.chains; + /// /// const result = await firstValueFrom( /// withChainHeadFollow({ - /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, + /// genesisHash: assetHub.genesisHash, /// }).pipe( /// mergeMap(({ genesisHash, followSubscriptionId }) => /// truapi.chain.stopHeadOperation({ @@ -278,10 +293,12 @@ pub trait Chain: Send + Sync { /// Fetch the canonical genesis hash for a chain. /// /// ```ts - /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; + /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); + /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); + /// const [assetHub] = chainInfo.value.chains; /// /// const result = await truapi.chain.getSpecGenesisHash({ - /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, + /// genesisHash: assetHub.genesisHash, /// }); /// assert(result.isOk(), "getSpecGenesisHash failed:", result); /// console.log("genesis hash:", result.value); @@ -299,10 +316,12 @@ pub trait Chain: Send + Sync { /// Fetch the display name of a chain. /// /// ```ts - /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; + /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); + /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); + /// const [assetHub] = chainInfo.value.chains; /// /// const result = await truapi.chain.getSpecChainName({ - /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, + /// genesisHash: assetHub.genesisHash, /// }); /// assert(result.isOk(), "getSpecChainName failed:", result); /// console.log("chain name:", result.value); @@ -319,10 +338,12 @@ pub trait Chain: Send + Sync { /// Fetch the JSON-encoded properties of a chain. /// /// ```ts - /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; + /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); + /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); + /// const [assetHub] = chainInfo.value.chains; /// /// const result = await truapi.chain.getSpecProperties({ - /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, + /// genesisHash: assetHub.genesisHash, /// }); /// assert(result.isOk(), "getSpecProperties failed:", result); /// console.log("chain properties:", result.value); @@ -339,10 +360,12 @@ pub trait Chain: Send + Sync { /// Broadcast a signed transaction. /// /// ```ts - /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; + /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); + /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); + /// const [assetHub] = chainInfo.value.chains; /// /// const result = await truapi.chain.broadcastTransaction({ - /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, + /// genesisHash: assetHub.genesisHash, /// transaction: "0x", /// }); /// assert(result.isOk(), "broadcastTransaction failed:", result); @@ -363,10 +386,12 @@ pub trait Chain: Send + Sync { /// Stop a transaction broadcast. /// /// ```ts - /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; + /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); + /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); + /// const [assetHub] = chainInfo.value.chains; /// /// const broadcast = await truapi.chain.broadcastTransaction({ - /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, + /// genesisHash: assetHub.genesisHash, /// transaction: "0x", /// }); /// assert(broadcast.isOk(), "broadcastTransaction failed:", broadcast); @@ -376,7 +401,7 @@ pub trait Chain: Send + Sync { /// ); /// /// const result = await truapi.chain.stopTransaction({ - /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, + /// genesisHash: assetHub.genesisHash, /// operationId: broadcast.value.operationId, /// }); /// assert(result.isOk(), "stopTransaction failed:", result); @@ -392,39 +417,23 @@ pub trait Chain: Send + Sync { Err(CallError::unavailable()) } - /// Enumerate the chains this host serves (RFC 0026). + /// Resolve chain identifiers to genesis hashes against the host's + /// configured environment (RFC 0026). /// /// ```ts - /// const result = await truapi.chain.getSupportedChains(); - /// assert(result.isOk(), "getSupportedChains failed:", result); + /// const result = await truapi.chain.getChainInfo({ + /// chains: ["AssetHub"], + /// }); + /// assert(result.isOk(), "getChainInfo failed:", result); /// console.log("network:", result.value.network); - /// console.log("supported chains:", result.value.chains); + /// console.log("asset hub genesis:", result.value.chains[0].genesisHash); /// ``` #[wire(request_id = 166)] - async fn get_supported_chains( - &self, - _cx: &CallContext, - _request: RemoteChainSupportedChainsRequest, - ) -> Result> - { - Err(CallError::unavailable()) - } - - /// Resolve a chain name to its genesis hash (RFC 0026). - /// - /// ```ts - /// const result = await truapi.chain.resolveChain({ - /// name: "asset-hub", - /// }); - /// assert(result.isOk(), "resolveChain failed:", result); - /// console.log("genesis hash:", result.value.genesisHash); - /// ``` - #[wire(request_id = 168)] - async fn resolve_chain( + async fn get_chain_info( &self, _cx: &CallContext, - _request: RemoteChainResolveChainRequest, - ) -> Result> { + _request: RemoteChainInfoRequest, + ) -> Result> { Err(CallError::unavailable()) } } diff --git a/rust/crates/truapi/src/api/signing.rs b/rust/crates/truapi/src/api/signing.rs index 273e848dc..3dcc91adf 100644 --- a/rust/crates/truapi/src/api/signing.rs +++ b/rust/crates/truapi/src/api/signing.rs @@ -21,14 +21,16 @@ pub trait Signing: Send + Sync { /// Construct a signed transaction for a product account. /// /// ```ts - /// import { PASEO_NEXT_V2_INDIVIDUALITY } from "@parity/truapi"; + /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["People"] }); + /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); + /// const [people] = chainInfo.value.chains; /// /// const payload = await buildCreateTransactionPayload({ /// signer: { /// dotNsIdentifier: "truapi-playground.dot", /// derivationIndex: { tag: "Left", value: 0 }, /// }, - /// genesisHash: PASEO_NEXT_V2_INDIVIDUALITY.genesis, + /// genesisHash: people.genesisHash, /// callData: "0x000000", /// }); /// assert(payload.isOk(), "buildCreateTransactionPayload failed:", payload); @@ -49,7 +51,9 @@ pub trait Signing: Send + Sync { /// Construct a signed transaction for a non-product (legacy) account. /// /// ```ts - /// import { PASEO_NEXT_V2_INDIVIDUALITY } from "@parity/truapi"; + /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["People"] }); + /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); + /// const [people] = chainInfo.value.chains; /// /// const accountResult = await truapi.account.getAccount({ /// productAccountId: { @@ -64,7 +68,7 @@ pub trait Signing: Send + Sync { /// dotNsIdentifier: "truapi-playground.dot", /// derivationIndex: { tag: "Left", value: 0 }, /// }, - /// genesisHash: PASEO_NEXT_V2_INDIVIDUALITY.genesis, + /// genesisHash: people.genesisHash, /// callData: "0x000000", /// }); /// assert(payload.isOk(), "buildCreateTransactionPayload failed:", payload); @@ -117,7 +121,9 @@ pub trait Signing: Send + Sync { /// Sign an extrinsic payload with a non-product account. /// /// ```ts - /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; + /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); + /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); + /// const [assetHub] = chainInfo.value.chains; /// /// const accountResult = await truapi.account.getAccount({ /// productAccountId: { @@ -133,7 +139,7 @@ pub trait Signing: Send + Sync { /// blockHash: "0xd6eec26135305a8ad257a20d003357284c8aa03d0bdb2b357ab0a22371e11ef2", /// blockNumber: "0x00000000", /// era: "0x00", - /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, + /// genesisHash: assetHub.genesisHash, /// method: "0x00003448656c6c6f2c20776f726c6421", /// nonce: "0x00000000", /// signedExtensions: [], @@ -185,7 +191,9 @@ pub trait Signing: Send + Sync { /// Sign an extrinsic payload. /// /// ```ts - /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; + /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); + /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); + /// const [assetHub] = chainInfo.value.chains; /// /// const result = await truapi.signing.signPayload({ /// account: { dotNsIdentifier: "truapi-playground.dot", derivationIndex: { tag: "Left", value: 0 } }, @@ -193,7 +201,7 @@ pub trait Signing: Send + Sync { /// blockHash: "0xd6eec26135305a8ad257a20d003357284c8aa03d0bdb2b357ab0a22371e11ef2", /// blockNumber: "0x00000000", /// era: "0x00", - /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, + /// genesisHash: assetHub.genesisHash, /// method: "0x00003448656c6c6f2c20776f726c6421", /// nonce: "0x00000000", /// signedExtensions: [], diff --git a/rust/crates/truapi/src/api/system.rs b/rust/crates/truapi/src/api/system.rs index b08c5da9c..792fd1cba 100644 --- a/rust/crates/truapi/src/api/system.rs +++ b/rust/crates/truapi/src/api/system.rs @@ -38,12 +38,14 @@ pub trait System: Send + Sync { /// Query whether the host supports a specific feature. /// /// ```ts - /// import { PASEO_NEXT_V2_ASSET_HUB } from "@parity/truapi"; + /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); + /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); + /// const [assetHub] = chainInfo.value.chains; /// /// const result = await truapi.system.featureSupported({ /// tag: "Chain", /// value: { - /// genesisHash: PASEO_NEXT_V2_ASSET_HUB.genesis, + /// genesisHash: assetHub.genesisHash, /// }, /// }); /// assert(result.isOk(), "featureSupported failed:", result); diff --git a/rust/crates/truapi/src/v01/chain.rs b/rust/crates/truapi/src/v01/chain.rs index f2d7c57f9..963f82230 100644 --- a/rust/crates/truapi/src/v01/chain.rs +++ b/rust/crates/truapi/src/v01/chain.rs @@ -356,43 +356,52 @@ pub struct RemoteChainTransactionBroadcastResponse { pub operation_id: Option, } -/// One chain a host serves. -#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] -pub struct HostChainDescriptor { - /// Stable machine key for the chain's role, e.g. "asset-hub". - pub name: String, - /// Genesis hash identifying the chain in all chain-scoped calls. - pub genesis_hash: Vec, +/// Role of a chain within the host's configured environment. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] +pub enum ChainIdentifier { + /// The relay chain. + Relay, + /// The asset hub system chain. + AssetHub, + /// The people chain. + People, + /// The bulletin chain. + Bulletin, } -/// Response listing every chain the host serves. +/// Resolved chain data for one requested [`ChainIdentifier`]. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] -pub struct RemoteChainSupportedChainsResponse { - /// Ecosystem the host is configured for, e.g. "polkadot", "kusama", "paseo". - pub network: String, - /// Complete set of chains available through this host. - pub chains: Vec, +pub struct ChainInfo { + /// Host-assigned chain name, e.g. "asset-hub". + pub name: String, + /// Genesis hash identifying the chain in all chain-scoped calls. + pub genesis_hash: [u8; 32], } -/// Request to resolve a named chain against the host's configured environment. +/// Request to resolve chain identifiers against the host's environment. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] -pub struct RemoteChainResolveChainRequest { - /// Stable machine key, e.g. "asset-hub". - pub name: String, +pub struct RemoteChainInfoRequest { + /// Chains to resolve. + pub chains: Vec, } -/// Response carrying the resolved genesis hash. +/// Response carrying one [`ChainInfo`] per requested identifier, in request order. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] -pub struct RemoteChainResolveChainResponse { - /// Genesis hash of the resolved chain. - pub genesis_hash: Vec, +pub struct RemoteChainInfoResponse { + /// Ecosystem the host is configured for, e.g. "polkadot", "kusama", "paseo". + pub network: String, + /// Resolved chains, aligned with the request's `chains`. + pub chains: Vec, } -/// Error from [`crate::api::Chain::resolve_chain`]. +/// Error from [`crate::api::Chain::get_chain_info`]. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] -pub enum RemoteChainResolveChainError { - /// No supported chain matches the requested name. - NotFound, +pub enum RemoteChainInfoError { + /// The host does not serve one of the requested chains. + NotSupported { + /// First requested identifier the host does not serve. + chain: ChainIdentifier, + }, /// Catch-all. Unknown(GenericError), } diff --git a/rust/crates/truapi/src/versioned/chain.rs b/rust/crates/truapi/src/versioned/chain.rs index b5342fe8b..d96275729 100644 --- a/rust/crates/truapi/src/versioned/chain.rs +++ b/rust/crates/truapi/src/versioned/chain.rs @@ -41,10 +41,7 @@ truapi_macros::versioned_type! { pub enum RemoteChainTransactionStopRequest { V1 => v01::RemoteChainTransactionStopRequest } pub enum RemoteChainTransactionStopResponse { V1 } pub enum RemoteChainTransactionStopError { V1 => v01::GenericError } - pub enum RemoteChainSupportedChainsRequest { V1 } - pub enum RemoteChainSupportedChainsResponse { V1 => v01::RemoteChainSupportedChainsResponse } - pub enum RemoteChainSupportedChainsError { V1 => v01::GenericError } - pub enum RemoteChainResolveChainRequest { V1 => v01::RemoteChainResolveChainRequest } - pub enum RemoteChainResolveChainResponse { V1 => v01::RemoteChainResolveChainResponse } - pub enum RemoteChainResolveChainError { V1 => v01::RemoteChainResolveChainError } + pub enum RemoteChainInfoRequest { V1 => v01::RemoteChainInfoRequest } + pub enum RemoteChainInfoResponse { V1 => v01::RemoteChainInfoResponse } + pub enum RemoteChainInfoError { V1 => v01::RemoteChainInfoError } } From d2e54e2ead9278b5f023733558edd2bc072cedd4 Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Fri, 7 Aug 2026 08:29:34 -0300 Subject: [PATCH 4/8] echo the identifier in ChainInfo instead of a host-minted name --- docs/rfcs/0026-supported-chains.md | 17 +++++++---------- rust/crates/truapi/src/v01/chain.rs | 11 ++++------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/docs/rfcs/0026-supported-chains.md b/docs/rfcs/0026-supported-chains.md index 758c27e91..20bbd9c6d 100644 --- a/docs/rfcs/0026-supported-chains.md +++ b/docs/rfcs/0026-supported-chains.md @@ -14,7 +14,7 @@ owner: "@valentinfernandez1" ## Summary -Add one method to the `Chain` trait. `get_chain_info` takes the chain identifiers a product wants to use, drawn from a closed role enum (`Relay`, `AssetHub`, `People`, `Bulletin`), and returns the ecosystem the host is configured for (for example `"paseo"`) plus one `ChainInfo` (name and genesis hash) per requested identifier, resolved against that environment. It is answered in-core from a single new platform syscall, so each host implements exactly one callback over configuration it already has. +Add one method to the `Chain` trait. `get_chain_info` takes the chain identifiers a product wants to use, drawn from a closed role enum (`Relay`, `AssetHub`, `People`, `Bulletin`), and returns the ecosystem the host is configured for (for example `"paseo"`) plus one `ChainInfo` (the echoed identifier and the genesis hash) per requested identifier, resolved against that environment. It is answered in-core from a single new platform syscall, so each host implements exactly one callback over configuration it already has. ## Motivation @@ -69,8 +69,8 @@ enum ChainIdentifier { /// Resolved chain data for one requested ChainIdentifier. struct ChainInfo { - /// Host-assigned chain name, e.g. "asset-hub". - name: String, + /// Identifier this entry resolves, echoed from the request. + identifier: ChainIdentifier, /// Genesis hash identifying the chain in all chain-scoped calls. genesis_hash: [u8; 32], } @@ -91,11 +91,8 @@ struct RemoteChainInfoResponse { /// Error from get_chain_info. enum RemoteChainInfoError { - /// The host does not serve one of the requested chains. - NotSupported { - /// First requested identifier the host does not serve. - chain: ChainIdentifier, - }, + /// The host does not serve the first named of the requested chains. + NotSupported(ChainIdentifier), /// Catch-all. Unknown(GenericError), } @@ -108,13 +105,13 @@ The request deliberately carries no network selector. A product does not get to ### Semantics and invariants - **Serviceability.** Every genesis hash returned by `get_chain_info` is a chain the host will serve `chain.*` and `signing.*` calls for. A `NotSupported` identifier will not be served. -- **Alignment.** The response's `chains` has exactly one entry per requested identifier, in request order, so products index it positionally. +- **Alignment.** The response's `chains` has exactly one entry per requested identifier, in request order. Each entry also echoes its identifier, so products can destructure positionally or match by identifier; neither requires trusting the other. - **All or nothing.** If any requested identifier is not served, the whole call fails with `NotSupported` naming the first such identifier; there are no partial responses. - **Stability.** An identifier resolves to the same chain for the lifetime of a connection. There is no subscription; a product observes host-side changes (such as a testnet wipe) by reconnecting. `network` is informational, not a selector: it tells a product or SDK which environment the host is running, so tooling can derive the environment from the host instead of asking the developer to configure it. It is an open ecosystem string ("polkadot", "kusama", "paseo", "devnet"), not a `Mainnet`/`Testnet` enum, because a binary flag cannot distinguish two testnets. -`ChainInfo` deliberately excludes display names and token properties. Once a product holds the genesis hash, that metadata is already reachable through `getSpecChainName` and `getSpecProperties`. +`ChainInfo` deliberately excludes host-assigned name strings, display names, and token properties. The echoed identifier already keys the entry unambiguously, and once a product holds the genesis hash the display metadata is reachable through `getSpecChainName` and `getSpecProperties`. ### Typical product flow diff --git a/rust/crates/truapi/src/v01/chain.rs b/rust/crates/truapi/src/v01/chain.rs index 963f82230..e85f3726d 100644 --- a/rust/crates/truapi/src/v01/chain.rs +++ b/rust/crates/truapi/src/v01/chain.rs @@ -372,8 +372,8 @@ pub enum ChainIdentifier { /// Resolved chain data for one requested [`ChainIdentifier`]. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct ChainInfo { - /// Host-assigned chain name, e.g. "asset-hub". - pub name: String, + /// Identifier this entry resolves, echoed from the request. + pub identifier: ChainIdentifier, /// Genesis hash identifying the chain in all chain-scoped calls. pub genesis_hash: [u8; 32], } @@ -397,11 +397,8 @@ pub struct RemoteChainInfoResponse { /// Error from [`crate::api::Chain::get_chain_info`]. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub enum RemoteChainInfoError { - /// The host does not serve one of the requested chains. - NotSupported { - /// First requested identifier the host does not serve. - chain: ChainIdentifier, - }, + /// The host does not serve the first named of the requested chains. + NotSupported(ChainIdentifier), /// Catch-all. Unknown(GenericError), } From 93bb63bade856cb70e479a1a44943ec957e16815 Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Fri, 7 Aug 2026 10:21:21 -0300 Subject: [PATCH 5/8] resolve one chain identifier per call --- docs/rfcs/0026-supported-chains.md | 62 ++++++++--------- rust/crates/truapi/src/api/account.rs | 14 ++-- rust/crates/truapi/src/api/chain.rs | 99 ++++++++++++--------------- rust/crates/truapi/src/api/signing.rs | 28 ++++---- rust/crates/truapi/src/api/system.rs | 7 +- rust/crates/truapi/src/v01/chain.rs | 27 +++----- 6 files changed, 102 insertions(+), 135 deletions(-) diff --git a/docs/rfcs/0026-supported-chains.md b/docs/rfcs/0026-supported-chains.md index 20bbd9c6d..3d7096c2c 100644 --- a/docs/rfcs/0026-supported-chains.md +++ b/docs/rfcs/0026-supported-chains.md @@ -14,7 +14,7 @@ owner: "@valentinfernandez1" ## Summary -Add one method to the `Chain` trait. `get_chain_info` takes the chain identifiers a product wants to use, drawn from a closed role enum (`Relay`, `AssetHub`, `People`, `Bulletin`), and returns the ecosystem the host is configured for (for example `"paseo"`) plus one `ChainInfo` (the echoed identifier and the genesis hash) per requested identifier, resolved against that environment. It is answered in-core from a single new platform syscall, so each host implements exactly one callback over configuration it already has. +Add one method to the `Chain` trait. `get_chain_info` takes one chain identifier, drawn from a closed role enum (`Relay`, `AssetHub`, `People`, `Bulletin`), and returns the ecosystem the host is configured for (for example `"paseo"`) plus the chain's genesis hash, resolved against that environment. It is answered in-core from a single new platform syscall, so each host implements exactly one callback over configuration it already has. Products needing several chains issue concurrent calls; the transport multiplexes them over one round trip. ## Motivation @@ -33,16 +33,16 @@ The fix is to make the host's chain set discoverable over the wire. Hosts alread ### `chain.getChainInfo` ```rust -/// Resolve chain identifiers to genesis hashes against the host's +/// Resolve a chain identifier to its genesis hash against the host's /// configured environment. /// /// ```ts /// const result = await truapi.chain.getChainInfo({ -/// chains: ["AssetHub"], +/// chain: "AssetHub", /// }); /// assert(result.isOk(), "getChainInfo failed:", result); /// console.log("network:", result.value.network); -/// console.log("asset hub genesis:", result.value.chains[0].genesisHash); +/// console.log("asset hub genesis:", result.value.genesisHash); /// ``` #[wire(request_id = 166)] async fn get_chain_info( @@ -67,63 +67,56 @@ enum ChainIdentifier { Bulletin, } -/// Resolved chain data for one requested ChainIdentifier. -struct ChainInfo { - /// Identifier this entry resolves, echoed from the request. - identifier: ChainIdentifier, - /// Genesis hash identifying the chain in all chain-scoped calls. - genesis_hash: [u8; 32], -} - -/// Request to resolve chain identifiers against the host's environment. +/// Request to resolve one chain identifier against the host's environment. struct RemoteChainInfoRequest { - /// Chains to resolve. - chains: Vec, + /// Chain to resolve. + chain: ChainIdentifier, } -/// Response carrying one ChainInfo per requested identifier, in request order. +/// Response carrying the resolved chain data. struct RemoteChainInfoResponse { /// Ecosystem the host is configured for, e.g. "polkadot", "kusama", "paseo". network: String, - /// Resolved chains, aligned with the request's `chains`. - chains: Vec, + /// Chain this response resolves, echoed from the request. + chain: ChainIdentifier, + /// Genesis hash identifying the chain in all chain-scoped calls. + genesis_hash: [u8; 32], } /// Error from get_chain_info. enum RemoteChainInfoError { - /// The host does not serve the first named of the requested chains. - NotSupported(ChainIdentifier), + /// The host does not serve the requested chain. + NotSupported, /// Catch-all. Unknown(GenericError), } ``` -The request is a batch: a product names every chain it needs in one call and gets them all back in one round trip. `ChainIdentifier` is a closed protocol enum of chain roles, not chain instances; the host maps each role to the concrete chain of its configured environment. Adding a new role is an additive enum variant. +`ChainIdentifier` is a closed protocol enum of chain roles, not chain instances; the host maps each role to the concrete chain of its configured environment. Adding a new role is an additive enum variant. The method resolves one identifier per call; the transport multiplexes concurrent requests, so a product needing several chains resolves them in parallel with no extra round trips and no batching semantics in the protocol. -The request deliberately carries no network selector. A product does not get to choose which network it operates on; the host is configured for exactly one environment (polkadot in production), and every identifier resolves against that. Asking the product to name the environment would reintroduce the guessing this RFC removes. +The request deliberately carries no network selector. A product does not get to choose which network it operates on; the host is configured for exactly one environment (polkadot in production), and the identifier resolves against that. Asking the product to name the environment would reintroduce the guessing this RFC removes. ### Semantics and invariants -- **Serviceability.** Every genesis hash returned by `get_chain_info` is a chain the host will serve `chain.*` and `signing.*` calls for. A `NotSupported` identifier will not be served. -- **Alignment.** The response's `chains` has exactly one entry per requested identifier, in request order. Each entry also echoes its identifier, so products can destructure positionally or match by identifier; neither requires trusting the other. -- **All or nothing.** If any requested identifier is not served, the whole call fails with `NotSupported` naming the first such identifier; there are no partial responses. +- **Serviceability.** A genesis hash returned by `get_chain_info` is a chain the host will serve `chain.*` and `signing.*` calls for. A `NotSupported` identifier will not be served. - **Stability.** An identifier resolves to the same chain for the lifetime of a connection. There is no subscription; a product observes host-side changes (such as a testnet wipe) by reconnecting. `network` is informational, not a selector: it tells a product or SDK which environment the host is running, so tooling can derive the environment from the host instead of asking the developer to configure it. It is an open ecosystem string ("polkadot", "kusama", "paseo", "devnet"), not a `Mainnet`/`Testnet` enum, because a binary flag cannot distinguish two testnets. -`ChainInfo` deliberately excludes host-assigned name strings, display names, and token properties. The echoed identifier already keys the entry unambiguously, and once a product holds the genesis hash the display metadata is reachable through `getSpecChainName` and `getSpecProperties`. +The response echoes the requested identifier so a response is self-describing in logs and debugging tools rather than only meaningful next to the request that produced it. It deliberately excludes host-assigned name strings, display names, and token properties: the identifier already keys the chain unambiguously, and once a product holds the genesis hash the display metadata is reachable through `getSpecChainName` and `getSpecProperties`. ### Typical product flow ```ts -const info = await truapi.chain.getChainInfo({ chains: ["AssetHub", "People"] }); -assert(info.isOk(), "getChainInfo failed:", info); - -const [assetHub, people] = info.value.chains; +const [assetHub, people] = await Promise.all([ + truapi.chain.getChainInfo({ chain: "AssetHub" }), + truapi.chain.getChainInfo({ chain: "People" }), +]); +assert(assetHub.isOk() && people.isOk(), "getChainInfo failed"); -const name = await truapi.chain.getSpecChainName({ genesisHash: assetHub.genesisHash }); +const name = await truapi.chain.getSpecChainName({ genesisHash: assetHub.value.genesisHash }); assert(name.isOk(), "getSpecChainName failed:", name); -console.log(`connected to ${name.value.chainName} on ${info.value.network}`); +console.log(`connected to ${name.value.chainName} on ${assetHub.value.network}`); ``` The product never embeds a hash. After a testnet wipe the host updates its config, the product reconnects, and the same code path picks up the new hash. @@ -133,7 +126,7 @@ The product never embeds a hash. After a testnet wipe the host updates its confi The core does not own the chain set. `system.featureSupported(Chain { genesis_hash })` is already a thin shim in `rust/crates/truapi-server/src/host_logic/features.rs` delegating to `truapi_platform::Features`, and `ChainProvider::connect(genesis_hash)` opens JSON-RPC pipes on demand. This RFC follows the same delegation pattern: - `truapi-platform` gains one syscall on `Features` returning the host's network string and its full identifier-to-chain mapping. -- `truapi-server` answers `get_chain_info` in-core from that syscall, resolving each requested identifier and mapping the first miss to `NotSupported`. +- `truapi-server` answers `get_chain_info` in-core from that syscall, resolving the requested identifier and mapping a miss to `NotSupported`. Hosts therefore implement exactly one callback, backed by configuration they already maintain. dotli's per-environment named slots (`relay`, `assethub`, `bulletin`, `people`, each with a genesis hash) map one-to-one onto `ChainIdentifier` variants; the iOS `TrUAPIHost` and the host CLI expose their equivalent config the same way. @@ -152,6 +145,7 @@ The change is purely additive: one new method with a fresh wire id, no changes t ## Alternatives - **Take chain names instead of `genesisHash` in every chain-scoped call.** This was discarded as it is a breaking change across the Rust trait, codegen, the TS client, dotli, the iOS host, and the product SDK. The genesis hash also remains necessary internally, since connections are keyed by it and signed payloads embed it via `CheckGenesis`. -- **Separate discovery and lookup methods (`getSupportedChains` + `resolveChain`).** This was discarded during review: a product that needs one chain should not fetch and filter the host's full mapping, and the batch request already covers the multi-chain case in one round trip. +- **Separate discovery and lookup methods (`getSupportedChains` + `resolveChain`).** This was discarded during review: a product that needs one chain should not fetch and filter the host's full mapping. +- **A batch request (`chains: Vec`).** This was discarded during review: the transport multiplexes concurrent requests, so batching adds ordering and partial-failure semantics without saving a round trip. A generalized batching layer, if ever needed, belongs to the transport and would cover every method. - **Free-form string identifiers.** This was discarded because names minted by host configuration form a de facto registry with no governance: two hosts could name the same chain differently, and typos fail only at runtime. The closed role enum is typo-proof, identical across hosts, and versioned with the protocol. - **A `network` selector on the request.** This was discarded because the product does not choose its network, the host's configuration does. Asking the product to name the environment would make it encode the environment again, which is the hard-coding this RFC removes. diff --git a/rust/crates/truapi/src/api/account.rs b/rust/crates/truapi/src/api/account.rs index c9780ab96..4f9e4f166 100644 --- a/rust/crates/truapi/src/api/account.rs +++ b/rust/crates/truapi/src/api/account.rs @@ -67,9 +67,8 @@ pub trait Account: Send + Sync { /// Retrieve the contextual alias for a context and ring. /// /// ```ts - /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["People"] }); - /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); - /// const [people] = chainInfo.value.chains; + /// const people = await truapi.chain.getChainInfo({ chain: "People" }); + /// assert(people.isOk(), "getChainInfo failed:", people); /// /// const PEOPLE_COLLECTION_ID = /// "0x706f703a706f6c6b61646f742e6e6574776f726b2f70656f706c652d6c697465"; @@ -77,7 +76,7 @@ pub trait Account: Send + Sync { /// const result = await truapi.account.getAccountAlias({ /// context: { productId: "truapi-playground.dot", suffix: { tag: "Left", value: 0 } }, /// ringLocation: { - /// chainId: people.genesisHash, + /// chainId: people.value.genesisHash, /// junctions: [ /// { tag: "PalletInstance", value: 67 }, /// { tag: "CollectionId", value: PEOPLE_COLLECTION_ID }, @@ -99,9 +98,8 @@ pub trait Account: Send + Sync { /// Generate a ring VRF proof; the host selects the member key for the ring. /// /// ```ts - /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["People"] }); - /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); - /// const [people] = chainInfo.value.chains; + /// const people = await truapi.chain.getChainInfo({ chain: "People" }); + /// assert(people.isOk(), "getChainInfo failed:", people); /// /// const PEOPLE_COLLECTION_ID = /// "0x706f703a706f6c6b61646f742e6e6574776f726b2f70656f706c652d6c697465"; @@ -109,7 +107,7 @@ pub trait Account: Send + Sync { /// const result = await truapi.account.createAccountProof({ /// context: { productId: "truapi-playground.dot", suffix: { tag: "Left", value: 0 } }, /// ringLocation: { - /// chainId: people.genesisHash, + /// chainId: people.value.genesisHash, /// junctions: [ /// { tag: "PalletInstance", value: 67 }, /// { tag: "CollectionId", value: PEOPLE_COLLECTION_ID }, diff --git a/rust/crates/truapi/src/api/chain.rs b/rust/crates/truapi/src/api/chain.rs index de12a8d7c..f08dbf531 100644 --- a/rust/crates/truapi/src/api/chain.rs +++ b/rust/crates/truapi/src/api/chain.rs @@ -30,15 +30,14 @@ pub trait Chain: Send + Sync { /// ```ts /// import { firstValueFrom, from } from "rxjs"; /// - /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); - /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); - /// const [assetHub] = chainInfo.value.chains; + /// const assetHub = await truapi.chain.getChainInfo({ chain: "AssetHub" }); + /// assert(assetHub.isOk(), "getChainInfo failed:", assetHub); /// /// const item = await firstValueFrom( /// from( /// truapi.chain.followHeadSubscribe({ /// request: { - /// genesisHash: assetHub.genesisHash, + /// genesisHash: assetHub.value.genesisHash, /// withRuntime: false, /// }, /// }), @@ -60,13 +59,12 @@ pub trait Chain: Send + Sync { /// ```ts /// import { firstValueFrom, mergeMap } from "rxjs"; /// - /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); - /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); - /// const [assetHub] = chainInfo.value.chains; + /// const assetHub = await truapi.chain.getChainInfo({ chain: "AssetHub" }); + /// assert(assetHub.isOk(), "getChainInfo failed:", assetHub); /// /// const result = await firstValueFrom( /// withChainHeadFollow({ - /// genesisHash: assetHub.genesisHash, + /// genesisHash: assetHub.value.genesisHash, /// }).pipe( /// mergeMap(({ genesisHash, followSubscriptionId, hash }) => /// truapi.chain.getHeadHeader({ genesisHash, followSubscriptionId, hash }), @@ -90,13 +88,12 @@ pub trait Chain: Send + Sync { /// ```ts /// import { firstValueFrom, mergeMap } from "rxjs"; /// - /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); - /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); - /// const [assetHub] = chainInfo.value.chains; + /// const assetHub = await truapi.chain.getChainInfo({ chain: "AssetHub" }); + /// assert(assetHub.isOk(), "getChainInfo failed:", assetHub); /// /// const result = await firstValueFrom( /// withChainHeadFollow({ - /// genesisHash: assetHub.genesisHash, + /// genesisHash: assetHub.value.genesisHash, /// }).pipe( /// mergeMap(({ genesisHash, followSubscriptionId, hash }) => /// truapi.chain.getHeadBody({ genesisHash, followSubscriptionId, hash }), @@ -120,13 +117,12 @@ pub trait Chain: Send + Sync { /// ```ts /// import { firstValueFrom, mergeMap } from "rxjs"; /// - /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); - /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); - /// const [assetHub] = chainInfo.value.chains; + /// const assetHub = await truapi.chain.getChainInfo({ chain: "AssetHub" }); + /// assert(assetHub.isOk(), "getChainInfo failed:", assetHub); /// /// const result = await firstValueFrom( /// withChainHeadFollow({ - /// genesisHash: assetHub.genesisHash, + /// genesisHash: assetHub.value.genesisHash, /// }).pipe( /// mergeMap(({ genesisHash, followSubscriptionId, hash }) => /// truapi.chain.getHeadStorage({ @@ -155,13 +151,12 @@ pub trait Chain: Send + Sync { /// ```ts /// import { firstValueFrom, mergeMap } from "rxjs"; /// - /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); - /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); - /// const [assetHub] = chainInfo.value.chains; + /// const assetHub = await truapi.chain.getChainInfo({ chain: "AssetHub" }); + /// assert(assetHub.isOk(), "getChainInfo failed:", assetHub); /// /// const result = await firstValueFrom( /// withChainHeadFollow({ - /// genesisHash: assetHub.genesisHash, + /// genesisHash: assetHub.value.genesisHash, /// withRuntime: true, /// }).pipe( /// mergeMap(({ genesisHash, followSubscriptionId, hash }) => @@ -192,13 +187,12 @@ pub trait Chain: Send + Sync { /// ```ts /// import { firstValueFrom, mergeMap } from "rxjs"; /// - /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); - /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); - /// const [assetHub] = chainInfo.value.chains; + /// const assetHub = await truapi.chain.getChainInfo({ chain: "AssetHub" }); + /// assert(assetHub.isOk(), "getChainInfo failed:", assetHub); /// /// const result = await firstValueFrom( /// withChainHeadFollow({ - /// genesisHash: assetHub.genesisHash, + /// genesisHash: assetHub.value.genesisHash, /// }).pipe( /// mergeMap(({ genesisHash, followSubscriptionId, hash }) => /// truapi.chain.unpinHead({ @@ -226,13 +220,12 @@ pub trait Chain: Send + Sync { /// ```ts /// import { firstValueFrom, mergeMap } from "rxjs"; /// - /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); - /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); - /// const [assetHub] = chainInfo.value.chains; + /// const assetHub = await truapi.chain.getChainInfo({ chain: "AssetHub" }); + /// assert(assetHub.isOk(), "getChainInfo failed:", assetHub); /// /// const result = await firstValueFrom( /// withChainHeadFollow({ - /// genesisHash: assetHub.genesisHash, + /// genesisHash: assetHub.value.genesisHash, /// }).pipe( /// mergeMap(({ genesisHash, followSubscriptionId }) => /// truapi.chain.continueHead({ @@ -260,13 +253,12 @@ pub trait Chain: Send + Sync { /// ```ts /// import { firstValueFrom, mergeMap } from "rxjs"; /// - /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); - /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); - /// const [assetHub] = chainInfo.value.chains; + /// const assetHub = await truapi.chain.getChainInfo({ chain: "AssetHub" }); + /// assert(assetHub.isOk(), "getChainInfo failed:", assetHub); /// /// const result = await firstValueFrom( /// withChainHeadFollow({ - /// genesisHash: assetHub.genesisHash, + /// genesisHash: assetHub.value.genesisHash, /// }).pipe( /// mergeMap(({ genesisHash, followSubscriptionId }) => /// truapi.chain.stopHeadOperation({ @@ -293,12 +285,11 @@ pub trait Chain: Send + Sync { /// Fetch the canonical genesis hash for a chain. /// /// ```ts - /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); - /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); - /// const [assetHub] = chainInfo.value.chains; + /// const assetHub = await truapi.chain.getChainInfo({ chain: "AssetHub" }); + /// assert(assetHub.isOk(), "getChainInfo failed:", assetHub); /// /// const result = await truapi.chain.getSpecGenesisHash({ - /// genesisHash: assetHub.genesisHash, + /// genesisHash: assetHub.value.genesisHash, /// }); /// assert(result.isOk(), "getSpecGenesisHash failed:", result); /// console.log("genesis hash:", result.value); @@ -316,12 +307,11 @@ pub trait Chain: Send + Sync { /// Fetch the display name of a chain. /// /// ```ts - /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); - /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); - /// const [assetHub] = chainInfo.value.chains; + /// const assetHub = await truapi.chain.getChainInfo({ chain: "AssetHub" }); + /// assert(assetHub.isOk(), "getChainInfo failed:", assetHub); /// /// const result = await truapi.chain.getSpecChainName({ - /// genesisHash: assetHub.genesisHash, + /// genesisHash: assetHub.value.genesisHash, /// }); /// assert(result.isOk(), "getSpecChainName failed:", result); /// console.log("chain name:", result.value); @@ -338,12 +328,11 @@ pub trait Chain: Send + Sync { /// Fetch the JSON-encoded properties of a chain. /// /// ```ts - /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); - /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); - /// const [assetHub] = chainInfo.value.chains; + /// const assetHub = await truapi.chain.getChainInfo({ chain: "AssetHub" }); + /// assert(assetHub.isOk(), "getChainInfo failed:", assetHub); /// /// const result = await truapi.chain.getSpecProperties({ - /// genesisHash: assetHub.genesisHash, + /// genesisHash: assetHub.value.genesisHash, /// }); /// assert(result.isOk(), "getSpecProperties failed:", result); /// console.log("chain properties:", result.value); @@ -360,12 +349,11 @@ pub trait Chain: Send + Sync { /// Broadcast a signed transaction. /// /// ```ts - /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); - /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); - /// const [assetHub] = chainInfo.value.chains; + /// const assetHub = await truapi.chain.getChainInfo({ chain: "AssetHub" }); + /// assert(assetHub.isOk(), "getChainInfo failed:", assetHub); /// /// const result = await truapi.chain.broadcastTransaction({ - /// genesisHash: assetHub.genesisHash, + /// genesisHash: assetHub.value.genesisHash, /// transaction: "0x", /// }); /// assert(result.isOk(), "broadcastTransaction failed:", result); @@ -386,12 +374,11 @@ pub trait Chain: Send + Sync { /// Stop a transaction broadcast. /// /// ```ts - /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); - /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); - /// const [assetHub] = chainInfo.value.chains; + /// const assetHub = await truapi.chain.getChainInfo({ chain: "AssetHub" }); + /// assert(assetHub.isOk(), "getChainInfo failed:", assetHub); /// /// const broadcast = await truapi.chain.broadcastTransaction({ - /// genesisHash: assetHub.genesisHash, + /// genesisHash: assetHub.value.genesisHash, /// transaction: "0x", /// }); /// assert(broadcast.isOk(), "broadcastTransaction failed:", broadcast); @@ -401,7 +388,7 @@ pub trait Chain: Send + Sync { /// ); /// /// const result = await truapi.chain.stopTransaction({ - /// genesisHash: assetHub.genesisHash, + /// genesisHash: assetHub.value.genesisHash, /// operationId: broadcast.value.operationId, /// }); /// assert(result.isOk(), "stopTransaction failed:", result); @@ -417,16 +404,16 @@ pub trait Chain: Send + Sync { Err(CallError::unavailable()) } - /// Resolve chain identifiers to genesis hashes against the host's + /// Resolve a chain identifier to its genesis hash against the host's /// configured environment (RFC 0026). /// /// ```ts /// const result = await truapi.chain.getChainInfo({ - /// chains: ["AssetHub"], + /// chain: "AssetHub", /// }); /// assert(result.isOk(), "getChainInfo failed:", result); /// console.log("network:", result.value.network); - /// console.log("asset hub genesis:", result.value.chains[0].genesisHash); + /// console.log("asset hub genesis:", result.value.genesisHash); /// ``` #[wire(request_id = 166)] async fn get_chain_info( diff --git a/rust/crates/truapi/src/api/signing.rs b/rust/crates/truapi/src/api/signing.rs index 3dcc91adf..e84b7c850 100644 --- a/rust/crates/truapi/src/api/signing.rs +++ b/rust/crates/truapi/src/api/signing.rs @@ -21,16 +21,15 @@ pub trait Signing: Send + Sync { /// Construct a signed transaction for a product account. /// /// ```ts - /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["People"] }); - /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); - /// const [people] = chainInfo.value.chains; + /// const people = await truapi.chain.getChainInfo({ chain: "People" }); + /// assert(people.isOk(), "getChainInfo failed:", people); /// /// const payload = await buildCreateTransactionPayload({ /// signer: { /// dotNsIdentifier: "truapi-playground.dot", /// derivationIndex: { tag: "Left", value: 0 }, /// }, - /// genesisHash: people.genesisHash, + /// genesisHash: people.value.genesisHash, /// callData: "0x000000", /// }); /// assert(payload.isOk(), "buildCreateTransactionPayload failed:", payload); @@ -51,9 +50,8 @@ pub trait Signing: Send + Sync { /// Construct a signed transaction for a non-product (legacy) account. /// /// ```ts - /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["People"] }); - /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); - /// const [people] = chainInfo.value.chains; + /// const people = await truapi.chain.getChainInfo({ chain: "People" }); + /// assert(people.isOk(), "getChainInfo failed:", people); /// /// const accountResult = await truapi.account.getAccount({ /// productAccountId: { @@ -68,7 +66,7 @@ pub trait Signing: Send + Sync { /// dotNsIdentifier: "truapi-playground.dot", /// derivationIndex: { tag: "Left", value: 0 }, /// }, - /// genesisHash: people.genesisHash, + /// genesisHash: people.value.genesisHash, /// callData: "0x000000", /// }); /// assert(payload.isOk(), "buildCreateTransactionPayload failed:", payload); @@ -121,9 +119,8 @@ pub trait Signing: Send + Sync { /// Sign an extrinsic payload with a non-product account. /// /// ```ts - /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); - /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); - /// const [assetHub] = chainInfo.value.chains; + /// const assetHub = await truapi.chain.getChainInfo({ chain: "AssetHub" }); + /// assert(assetHub.isOk(), "getChainInfo failed:", assetHub); /// /// const accountResult = await truapi.account.getAccount({ /// productAccountId: { @@ -139,7 +136,7 @@ pub trait Signing: Send + Sync { /// blockHash: "0xd6eec26135305a8ad257a20d003357284c8aa03d0bdb2b357ab0a22371e11ef2", /// blockNumber: "0x00000000", /// era: "0x00", - /// genesisHash: assetHub.genesisHash, + /// genesisHash: assetHub.value.genesisHash, /// method: "0x00003448656c6c6f2c20776f726c6421", /// nonce: "0x00000000", /// signedExtensions: [], @@ -191,9 +188,8 @@ pub trait Signing: Send + Sync { /// Sign an extrinsic payload. /// /// ```ts - /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); - /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); - /// const [assetHub] = chainInfo.value.chains; + /// const assetHub = await truapi.chain.getChainInfo({ chain: "AssetHub" }); + /// assert(assetHub.isOk(), "getChainInfo failed:", assetHub); /// /// const result = await truapi.signing.signPayload({ /// account: { dotNsIdentifier: "truapi-playground.dot", derivationIndex: { tag: "Left", value: 0 } }, @@ -201,7 +197,7 @@ pub trait Signing: Send + Sync { /// blockHash: "0xd6eec26135305a8ad257a20d003357284c8aa03d0bdb2b357ab0a22371e11ef2", /// blockNumber: "0x00000000", /// era: "0x00", - /// genesisHash: assetHub.genesisHash, + /// genesisHash: assetHub.value.genesisHash, /// method: "0x00003448656c6c6f2c20776f726c6421", /// nonce: "0x00000000", /// signedExtensions: [], diff --git a/rust/crates/truapi/src/api/system.rs b/rust/crates/truapi/src/api/system.rs index 792fd1cba..93d909631 100644 --- a/rust/crates/truapi/src/api/system.rs +++ b/rust/crates/truapi/src/api/system.rs @@ -38,14 +38,13 @@ pub trait System: Send + Sync { /// Query whether the host supports a specific feature. /// /// ```ts - /// const chainInfo = await truapi.chain.getChainInfo({ chains: ["AssetHub"] }); - /// assert(chainInfo.isOk(), "getChainInfo failed:", chainInfo); - /// const [assetHub] = chainInfo.value.chains; + /// const assetHub = await truapi.chain.getChainInfo({ chain: "AssetHub" }); + /// assert(assetHub.isOk(), "getChainInfo failed:", assetHub); /// /// const result = await truapi.system.featureSupported({ /// tag: "Chain", /// value: { - /// genesisHash: assetHub.genesisHash, + /// genesisHash: assetHub.value.genesisHash, /// }, /// }); /// assert(result.isOk(), "featureSupported failed:", result); diff --git a/rust/crates/truapi/src/v01/chain.rs b/rust/crates/truapi/src/v01/chain.rs index e85f3726d..85704960f 100644 --- a/rust/crates/truapi/src/v01/chain.rs +++ b/rust/crates/truapi/src/v01/chain.rs @@ -369,36 +369,29 @@ pub enum ChainIdentifier { Bulletin, } -/// Resolved chain data for one requested [`ChainIdentifier`]. -#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] -pub struct ChainInfo { - /// Identifier this entry resolves, echoed from the request. - pub identifier: ChainIdentifier, - /// Genesis hash identifying the chain in all chain-scoped calls. - pub genesis_hash: [u8; 32], -} - -/// Request to resolve chain identifiers against the host's environment. +/// Request to resolve one chain identifier against the host's environment. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct RemoteChainInfoRequest { - /// Chains to resolve. - pub chains: Vec, + /// Chain to resolve. + pub chain: ChainIdentifier, } -/// Response carrying one [`ChainInfo`] per requested identifier, in request order. +/// Response carrying the resolved chain data. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct RemoteChainInfoResponse { /// Ecosystem the host is configured for, e.g. "polkadot", "kusama", "paseo". pub network: String, - /// Resolved chains, aligned with the request's `chains`. - pub chains: Vec, + /// Chain this response resolves, echoed from the request. + pub chain: ChainIdentifier, + /// Genesis hash identifying the chain in all chain-scoped calls. + pub genesis_hash: [u8; 32], } /// Error from [`crate::api::Chain::get_chain_info`]. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub enum RemoteChainInfoError { - /// The host does not serve the first named of the requested chains. - NotSupported(ChainIdentifier), + /// The host does not serve the requested chain. + NotSupported, /// Catch-all. Unknown(GenericError), } From 5fc56b3e430359f3bc04086096706a9deb6d57ad Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Fri, 7 Aug 2026 12:27:06 -0300 Subject: [PATCH 6/8] feat(server): implement chain.getChainInfo in the core --- js/packages/truapi-host/src/test-support.ts | 5 +- .../tests/golden/host-callbacks-adapter.ts | 4 + .../tests/golden/host-callbacks.ts | 62 ++++++++++ .../tests/golden/wasm_bridge.rs | 13 +++ .../tests/golden/worker-callbacks.ts | 5 + rust/crates/truapi-host-cli/src/platform.rs | 6 + rust/crates/truapi-platform/src/lib.rs | 39 +++++-- .../truapi-server/src/host_logic/features.rs | 96 ++++++++++++++- rust/crates/truapi-server/src/native.rs | 109 ++++++++++++++++++ rust/crates/truapi-server/src/runtime.rs | 52 ++++++++- rust/crates/truapi-server/src/test_support.rs | 10 ++ .../src/wasm/generated_bridge.rs | 13 +++ rust/crates/truapi-server/tests/common/mod.rs | 10 ++ .../truapi-server/tests/wire_result_shape.rs | 57 +++++++++ rust/crates/truapi/src/lib.rs | 10 +- 15 files changed, 476 insertions(+), 15 deletions(-) diff --git a/js/packages/truapi-host/src/test-support.ts b/js/packages/truapi-host/src/test-support.ts index 6964e2e46..c479cdc25 100644 --- a/js/packages/truapi-host/src/test-support.ts +++ b/js/packages/truapi-host/src/test-support.ts @@ -21,7 +21,10 @@ export function makeHostCallbacks( devicePermission: async () => ({ granted: false }), remotePermission: async () => ({ granted: false }), }, - features: { featureSupported: async () => ({ supported: false }) }, + features: { + featureSupported: async () => ({ supported: false }), + supportedChains: async () => ({ network: "paseo", chains: [] }), + }, productStorage: { read: async () => undefined, write: async () => {}, diff --git a/rust/crates/truapi-codegen/tests/golden/host-callbacks-adapter.ts b/rust/crates/truapi-codegen/tests/golden/host-callbacks-adapter.ts index 4a6ff49d5..54b57927f 100644 --- a/rust/crates/truapi-codegen/tests/golden/host-callbacks-adapter.ts +++ b/rust/crates/truapi-codegen/tests/golden/host-callbacks-adapter.ts @@ -20,6 +20,7 @@ import type { GenericError, NotificationId } from "@parity/truapi"; import { AuthState, CoreStorageKey, + HostChainSet, UserConfirmationReview, } from "./host-callbacks.js"; import type { RequiredHostCallbacks } from "./host-callbacks.js"; @@ -34,6 +35,7 @@ export interface RawCallbacks { writeCoreStorage(key: Uint8Array, value: Uint8Array): Promise; clearCoreStorage(key: Uint8Array): Promise; featureSupported(request: Uint8Array): Promise; + supportedChains(): Promise; navigateTo(url: string): Promise; pushNotification(notification: Uint8Array): Promise; cancelNotification(id: NotificationId): Promise; @@ -77,6 +79,8 @@ export function createWasmRawCallbacks( HostFeatureSupportedRequest.dec(request), ), ), + supportedChains: async () => + HostChainSet.enc(await callbacks.features.supportedChains()), navigateTo: async (url) => await callbacks.navigation.navigateTo(url), pushNotification: async (notification) => HostPushNotificationResponse.enc( diff --git a/rust/crates/truapi-codegen/tests/golden/host-callbacks.ts b/rust/crates/truapi-codegen/tests/golden/host-callbacks.ts index 98d2aa1c7..498050d66 100644 --- a/rust/crates/truapi-codegen/tests/golden/host-callbacks.ts +++ b/rust/crates/truapi-codegen/tests/golden/host-callbacks.ts @@ -8,6 +8,7 @@ import * as S from "@parity/truapi/scale"; import { AllocatableResource, + ChainIdentifier, HostAccountSignVrfRequest, HostDevicePermissionRequest, HostSignPayloadRequest, @@ -180,6 +181,37 @@ export type CreateTransactionReview = */ | { tag: "LegacyAccount"; value: LegacyAccountTxPayload }; +/** + * One chain a host serves: a protocol chain role mapped to the concrete + * chain of the host's configured environment. + */ +export interface HostChainEntry { + /** + * Protocol role this entry answers for. + */ + identifier: ChainIdentifier; + + /** + * Genesis hash identifying the chain in all chain-scoped calls. + */ + genesisHash: Uint8Array; +} + +/** + * The chain set a host serves: its environment plus one entry per chain role. + */ +export interface HostChainSet { + /** + * Ecosystem the host is configured for, e.g. "polkadot", "paseo". + */ + network: string; + + /** + * Complete set of chains available through this host. + */ + chains: Array; +} + /** * Review shown before a product learns the user's primary identity. */ @@ -477,6 +509,29 @@ export const CreateTransactionReview: S.Codec = S.lazy( }), ); +/** + * One chain a host serves: a protocol chain role mapped to the concrete + * chain of the host's configured environment. + */ +export const HostChainEntry: S.Codec = S.lazy( + (): S.Codec => + S.Struct({ + identifier: ChainIdentifier, + genesisHash: S.Bytes(32), + }) as S.Codec, +); + +/** + * The chain set a host serves: its environment plus one entry per chain role. + */ +export const HostChainSet: S.Codec = S.lazy( + (): S.Codec => + S.Struct({ + network: S.str, + chains: S.Vector(HostChainEntry), + }) as S.Codec, +); + /** * Review shown before a product learns the user's primary identity. */ @@ -718,6 +773,13 @@ export interface Features { featureSupported( request: HostFeatureSupportedRequest, ): Promise; + + /** + * Enumerate the chains this host serves (RFC 0026). The returned set must + * match exactly what `ChainProvider::connect` will accept; the core + * resolves `get_chain_info` requests against it. + */ + supportedChains(): Promise; } /** diff --git a/rust/crates/truapi-codegen/tests/golden/wasm_bridge.rs b/rust/crates/truapi-codegen/tests/golden/wasm_bridge.rs index c08a48688..322f3068e 100644 --- a/rust/crates/truapi-codegen/tests/golden/wasm_bridge.rs +++ b/rust/crates/truapi-codegen/tests/golden/wasm_bridge.rs @@ -27,6 +27,7 @@ pub(super) struct JsBridge { pub(super) write_core_storage: Function, pub(super) clear_core_storage: Function, pub(super) feature_supported: Function, + pub(super) supported_chains: Function, pub(super) navigate_to: Function, pub(super) push_notification: Function, pub(super) cancel_notification: Function, @@ -49,6 +50,7 @@ impl JsBridge { write_core_storage: get_function(callbacks, "writeCoreStorage")?, clear_core_storage: get_function(callbacks, "clearCoreStorage")?, feature_supported: get_function(callbacks, "featureSupported")?, + supported_chains: get_function(callbacks, "supportedChains")?, navigate_to: get_function(callbacks, "navigateTo")?, push_notification: get_function(callbacks, "pushNotification")?, cancel_notification: get_function(callbacks, "cancelNotification")?, @@ -137,6 +139,17 @@ impl truapi_platform::Features for WasmPlatform { ) .map_err(generic) } + + async fn supported_chains(&self) -> Result { + let bytes = invoke_bytes_return(&self.bridge.supported_chains, Vec::new()) + .await + .map_err(generic)?; + decode_bytes::( + bytes, + "supportedChains response did not decode", + ) + .map_err(generic) + } } #[truapi_platform::async_trait] diff --git a/rust/crates/truapi-codegen/tests/golden/worker-callbacks.ts b/rust/crates/truapi-codegen/tests/golden/worker-callbacks.ts index 5ce66c4ae..baa79b3b4 100644 --- a/rust/crates/truapi-codegen/tests/golden/worker-callbacks.ts +++ b/rust/crates/truapi-codegen/tests/golden/worker-callbacks.ts @@ -16,6 +16,7 @@ export const CALLBACK_NAMES = [ "writeCoreStorage", "clearCoreStorage", "featureSupported", + "supportedChains", "navigateTo", "pushNotification", "cancelNotification", @@ -67,6 +68,10 @@ function rawCallbacks( bridge.callbackRequest("featureSupported", [request]) as ReturnType< RawCallbacks["featureSupported"] >, + supportedChains: () => + bridge.callbackRequest("supportedChains", []) as ReturnType< + RawCallbacks["supportedChains"] + >, navigateTo: (url) => bridge.callbackRequest("navigateTo", [url]) as ReturnType< RawCallbacks["navigateTo"] diff --git a/rust/crates/truapi-host-cli/src/platform.rs b/rust/crates/truapi-host-cli/src/platform.rs index 947f857b8..a5bf984bd 100644 --- a/rust/crates/truapi-host-cli/src/platform.rs +++ b/rust/crates/truapi-host-cli/src/platform.rs @@ -612,6 +612,12 @@ impl Features for CliPlatform { ) -> Result { Ok(api::HostFeatureSupportedResponse { supported: false }) } + + async fn supported_chains(&self) -> Result { + Err(api::GenericError { + reason: "the CLI host serves no product chains".to_string(), + }) + } } impl truapi_platform::AuthPresenter for CliPlatform { diff --git a/rust/crates/truapi-platform/src/lib.rs b/rust/crates/truapi-platform/src/lib.rs index cbf5e2e8d..a0aa9c8c5 100644 --- a/rust/crates/truapi-platform/src/lib.rs +++ b/rust/crates/truapi-platform/src/lib.rs @@ -16,13 +16,14 @@ use unicode_normalization::UnicodeNormalization; pub use async_trait::async_trait; use truapi::latest::{ - AllocatableResource, GenericError, HostDevicePermissionRequest, HostDevicePermissionResponse, - HostFeatureSupportedRequest, HostFeatureSupportedResponse, HostLocalStorageReadError, - HostNavigateToError, HostPushNotificationRequest, HostPushNotificationResponse, - HostSignPayloadRequest, HostSignPayloadWithLegacyAccountRequest, HostSignRawRequest, - HostSignRawWithLegacyAccountRequest, LegacyAccountTxPayload, NotificationId, ProductAccountId, - ProductAccountTxPayload, ProductProofContext, RemotePermission, RemotePermissionRequest, - RemotePermissionResponse, RingLocation, ThemeVariant, + AllocatableResource, ChainIdentifier, GenericError, HostDevicePermissionRequest, + HostDevicePermissionResponse, HostFeatureSupportedRequest, HostFeatureSupportedResponse, + HostLocalStorageReadError, HostNavigateToError, HostPushNotificationRequest, + HostPushNotificationResponse, HostSignPayloadRequest, HostSignPayloadWithLegacyAccountRequest, + HostSignRawRequest, HostSignRawWithLegacyAccountRequest, LegacyAccountTxPayload, + NotificationId, ProductAccountId, ProductAccountTxPayload, ProductProofContext, + RemotePermission, RemotePermissionRequest, RemotePermissionResponse, RingLocation, + ThemeVariant, }; use truapi::v01::HostAccountSignVrfRequest; use url::Url; @@ -463,6 +464,25 @@ pub trait PairingHostAdmin: Send + Sync { fn notify_session_store_changed(&self); } +/// One chain a host serves: a protocol chain role mapped to the concrete +/// chain of the host's configured environment. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct HostChainEntry { + /// Protocol role this entry answers for. + pub identifier: ChainIdentifier, + /// Genesis hash identifying the chain in all chain-scoped calls. + pub genesis_hash: [u8; 32], +} + +/// The chain set a host serves: its environment plus one entry per chain role. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct HostChainSet { + /// Ecosystem the host is configured for, e.g. "polkadot", "paseo". + pub network: String, + /// Complete set of chains available through this host. + pub chains: Vec, +} + /// Feature-support probing. The host answers whether it can service a given /// capability (currently scoped to per-chain support). #[async_trait] @@ -472,6 +492,11 @@ pub trait Features: Send + Sync { &self, request: HostFeatureSupportedRequest, ) -> Result; + + /// Enumerate the chains this host serves (RFC 0026). The returned set must + /// match exactly what [`ChainProvider::connect`] will accept; the core + /// resolves `get_chain_info` requests against it. + async fn supported_chains(&self) -> Result; } /// JSON-RPC provider factory for chain access. diff --git a/rust/crates/truapi-server/src/host_logic/features.rs b/rust/crates/truapi-server/src/host_logic/features.rs index a71a106ba..16a73ded5 100644 --- a/rust/crates/truapi-server/src/host_logic/features.rs +++ b/rust/crates/truapi-server/src/host_logic/features.rs @@ -1,11 +1,14 @@ //! Feature-detection delegation. //! -//! `feature_supported` is a platform syscall: each host owns the set of -//! chains it can service. This module is a thin shim that forwards the -//! request through to [`truapi_platform::Features`]. +//! `feature_supported` and `supported_chains` are platform syscalls: each +//! host owns the set of chains it can service. This module is a thin shim +//! that forwards through to [`truapi_platform::Features`], plus the in-core +//! RFC-0026 resolution that answers `get_chain_info` from the host's chain +//! set so per-request semantics (ordering, `NotSupported`) stay core-owned. +use truapi::latest::{RemoteChainInfoError, RemoteChainInfoRequest, RemoteChainInfoResponse}; use truapi::v01::{GenericError, HostFeatureSupportedRequest, HostFeatureSupportedResponse}; -use truapi_platform::Features; +use truapi_platform::{Features, HostChainSet}; /// Forward a feature-support query to the platform implementation. pub async fn feature_supported( @@ -15,9 +18,52 @@ pub async fn feature_supported( platform.feature_supported(request).await } +/// Fetch the host's chain set from the platform implementation. +pub async fn supported_chains( + platform: &P, +) -> Result { + platform.supported_chains().await +} + +/// Resolve a `get_chain_info` request against the host's chain set: the +/// requested identifier's genesis hash plus the host's network, echoing the +/// identifier, or `NotSupported` when the host does not serve it. +pub fn chain_info( + set: &HostChainSet, + request: &RemoteChainInfoRequest, +) -> Result { + set.chains + .iter() + .find(|entry| entry.identifier == request.chain) + .map(|entry| RemoteChainInfoResponse { + network: set.network.clone(), + chain: entry.identifier, + genesis_hash: entry.genesis_hash, + }) + .ok_or(RemoteChainInfoError::NotSupported) +} + #[cfg(test)] mod tests { use super::*; + use truapi::latest::ChainIdentifier; + use truapi_platform::HostChainEntry; + + fn paseo_set() -> HostChainSet { + HostChainSet { + network: "paseo".to_string(), + chains: vec![ + HostChainEntry { + identifier: ChainIdentifier::AssetHub, + genesis_hash: [0xaa; 32], + }, + HostChainEntry { + identifier: ChainIdentifier::People, + genesis_hash: [0xbb; 32], + }, + ], + } + } struct AlwaysSupported; @@ -30,6 +76,10 @@ mod tests { assert!(matches!(request, HostFeatureSupportedRequest::Chain { .. })); Ok(HostFeatureSupportedResponse { supported: true }) } + + async fn supported_chains(&self) -> Result { + Ok(paseo_set()) + } } struct AlwaysUnsupported; @@ -43,6 +93,12 @@ mod tests { assert!(matches!(request, HostFeatureSupportedRequest::Chain { .. })); Ok(HostFeatureSupportedResponse { supported: false }) } + + async fn supported_chains(&self) -> Result { + Err(GenericError { + reason: "no chains".to_string(), + }) + } } fn req() -> HostFeatureSupportedRequest { @@ -63,4 +119,36 @@ mod tests { futures::executor::block_on(feature_supported(&AlwaysUnsupported, req())).unwrap(); assert!(!resp.supported); } + + #[test] + fn delegates_supported_chains_to_platform() { + let set = futures::executor::block_on(supported_chains(&AlwaysSupported)).unwrap(); + assert_eq!(set, paseo_set()); + } + + #[test] + fn surfaces_supported_chains_platform_error() { + let err = futures::executor::block_on(supported_chains(&AlwaysUnsupported)).unwrap_err(); + assert_eq!(err.reason, "no chains"); + } + + #[test] + fn resolves_identifier_and_echoes_it() { + let request = RemoteChainInfoRequest { + chain: ChainIdentifier::People, + }; + let response = chain_info(&paseo_set(), &request).unwrap(); + assert_eq!(response.network, "paseo"); + assert_eq!(response.chain, ChainIdentifier::People); + assert_eq!(response.genesis_hash, [0xbb; 32]); + } + + #[test] + fn unserved_identifier_is_not_supported() { + let request = RemoteChainInfoRequest { + chain: ChainIdentifier::Bulletin, + }; + let err = chain_info(&paseo_set(), &request).unwrap_err(); + assert_eq!(err, RemoteChainInfoError::NotSupported); + } } diff --git a/rust/crates/truapi-server/src/native.rs b/rust/crates/truapi-server/src/native.rs index 51f3da5ff..c67a05b12 100644 --- a/rust/crates/truapi-server/src/native.rs +++ b/rust/crates/truapi-server/src/native.rs @@ -398,6 +398,79 @@ impl From for PushNotificationRequest { } } +/// Native-friendly mirror of [`v01::ChainIdentifier`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)] +pub enum ChainIdentifier { + /// The relay chain. + Relay, + /// The asset hub system chain. + AssetHub, + /// The people chain. + People, + /// The bulletin chain. + Bulletin, +} + +impl From for v01::ChainIdentifier { + fn from(identifier: ChainIdentifier) -> Self { + match identifier { + ChainIdentifier::Relay => Self::Relay, + ChainIdentifier::AssetHub => Self::AssetHub, + ChainIdentifier::People => Self::People, + ChainIdentifier::Bulletin => Self::Bulletin, + } + } +} + +/// Native-friendly mirror of one [`truapi_platform::HostChainEntry`]. +#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)] +pub struct ChainEntry { + /// Protocol role this entry answers for. + pub identifier: ChainIdentifier, + /// 32-byte genesis hash identifying the chain in all chain-scoped calls. + pub genesis_hash: Vec, +} + +/// Native-friendly mirror of [`truapi_platform::HostChainSet`]. +#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)] +pub struct SupportedChains { + /// Ecosystem the host is configured for, e.g. "polkadot", "paseo". + pub network: String, + /// Complete set of chains available through this host. + pub chains: Vec, +} + +impl TryFrom for truapi_platform::HostChainSet { + type Error = v01::GenericError; + + fn try_from(response: SupportedChains) -> Result { + let chains = + response + .chains + .into_iter() + .map(|entry| { + let genesis_hash: [u8; 32] = entry.genesis_hash.try_into().map_err( + |bad: Vec| v01::GenericError { + reason: format!( + "supported_chains genesis hash for {:?} has {} bytes, expected 32", + entry.identifier, + bad.len() + ), + }, + )?; + Ok(truapi_platform::HostChainEntry { + identifier: entry.identifier.into(), + genesis_hash, + }) + }) + .collect::, v01::GenericError>>()?; + Ok(Self { + network: response.network, + chains, + }) + } +} + /// Native-friendly mirror of [`v01::HostFeatureSupportedRequest`]. #[derive(Debug, Clone, PartialEq, Eq, uniffi::Enum)] pub enum FeatureSupportedRequest { @@ -753,6 +826,11 @@ pub trait HostCallbacks: Send + Sync { request: FeatureSupportedRequest, ) -> Result; + /// Enumerate the chains this host serves (RFC 0026): its environment plus + /// one entry per chain role. The returned set must match exactly what + /// `chain_connect` will accept. + async fn supported_chains(&self) -> Result; + /// Read a value from the host's scoped key-value store. fn local_storage_read(&self, key: String) -> Result>, HostStorageError>; /// Write a value to the host's scoped key-value store. @@ -1191,6 +1269,19 @@ impl Features for CallbackPlatform { .map_err(v01::GenericError::from)?; Ok(v01::HostFeatureSupportedResponse { supported }) } + + async fn supported_chains(&self) -> Result { + self.callbacks.on_core_log( + "truapi.native.callback.supported_chains".to_string(), + String::new(), + ); + + self.callbacks + .supported_chains() + .await + .map_err(v01::GenericError::from)? + .try_into() + } } #[async_trait] @@ -1497,6 +1588,12 @@ mod tests { ) -> Result { Ok(false) } + async fn supported_chains(&self) -> Result { + Ok(SupportedChains { + network: "paseo".to_string(), + chains: Vec::new(), + }) + } fn local_storage_read(&self, _key: String) -> Result>, HostStorageError> { Ok(None) } @@ -1835,6 +1932,12 @@ mod tests { ) -> Result { Ok(false) } + async fn supported_chains(&self) -> Result { + Ok(SupportedChains { + network: "paseo".to_string(), + chains: Vec::new(), + }) + } fn local_storage_read( &self, _key: String, @@ -1974,6 +2077,12 @@ mod tests { ) -> Result { Ok(true) } + async fn supported_chains(&self) -> Result { + Ok(SupportedChains { + network: "paseo".to_string(), + chains: Vec::new(), + }) + } fn local_storage_read( &self, _key: String, diff --git a/rust/crates/truapi-server/src/runtime.rs b/rust/crates/truapi-server/src/runtime.rs index 632cd0086..1a2be2332 100644 --- a/rust/crates/truapi-server/src/runtime.rs +++ b/rust/crates/truapi-server/src/runtime.rs @@ -40,7 +40,7 @@ use web_time::Instant; use crate::chain_runtime::RuntimeFailure; use crate::host_logic::bulletin::preimage_key; use crate::host_logic::dotns::{NavigateDecision, parse_navigate}; -use crate::host_logic::features::feature_supported; +use crate::host_logic::features::{chain_info, feature_supported, supported_chains}; use crate::host_logic::permissions::PermissionsService; #[cfg(test)] use crate::host_logic::product_account::index_bytes; @@ -97,6 +97,7 @@ use truapi::versioned::chain::{ RemoteChainHeadStopOperationRequest, RemoteChainHeadStopOperationResponse, RemoteChainHeadStorageError, RemoteChainHeadStorageRequest, RemoteChainHeadStorageResponse, RemoteChainHeadUnpinError, RemoteChainHeadUnpinRequest, RemoteChainHeadUnpinResponse, + RemoteChainInfoError, RemoteChainInfoRequest, RemoteChainInfoResponse, RemoteChainSpecChainNameError, RemoteChainSpecChainNameRequest, RemoteChainSpecChainNameResponse, RemoteChainSpecGenesisHashError, RemoteChainSpecGenesisHashRequest, RemoteChainSpecGenesisHashResponse, @@ -1885,6 +1886,25 @@ impl Chain for ProductRuntimeHost { .map(|()| RemoteChainTransactionStopResponse::V1) .map_err(runtime_failure_to_call_error) } + + #[instrument(skip_all, fields(runtime.method = "chain.get_chain_info"))] + async fn get_chain_info( + &self, + _cx: &CallContext, + request: RemoteChainInfoRequest, + ) -> Result> { + let RemoteChainInfoRequest::V1(inner) = request; + let set = supported_chains(self.services.platform.as_ref()) + .await + .map_err(|err| { + CallError::Domain(RemoteChainInfoError::V1( + truapi::latest::RemoteChainInfoError::Unknown(err), + )) + })?; + chain_info(&set, &inner) + .map(RemoteChainInfoResponse::V1) + .map_err(|err| CallError::Domain(RemoteChainInfoError::V1(err))) + } } // --------------------------------------------------------------------------- @@ -2410,6 +2430,36 @@ mod tests { assert!(inner.supported); } + #[test] + fn get_chain_info_round_trips_through_runtime() { + let host = ProductRuntimeHost::new_compat(stub_platform(), test_spawner()); + let cx = CallContext::default(); + let request = RemoteChainInfoRequest::V1(v01::RemoteChainInfoRequest { + chain: v01::ChainIdentifier::AssetHub, + }); + let response = futures::executor::block_on(host.get_chain_info(&cx, request)).unwrap(); + let RemoteChainInfoResponse::V1(inner) = response; + assert_eq!(inner.network, "paseo"); + assert_eq!(inner.chain, v01::ChainIdentifier::AssetHub); + assert_eq!(inner.genesis_hash, [0xaa; 32]); + } + + #[test] + fn get_chain_info_unserved_identifier_is_not_supported() { + let host = ProductRuntimeHost::new_compat(stub_platform(), test_spawner()); + let cx = CallContext::default(); + let request = RemoteChainInfoRequest::V1(v01::RemoteChainInfoRequest { + chain: v01::ChainIdentifier::Bulletin, + }); + let error = futures::executor::block_on(host.get_chain_info(&cx, request)).unwrap_err(); + assert_eq!( + error, + CallError::Domain(RemoteChainInfoError::V1( + v01::RemoteChainInfoError::NotSupported + )) + ); + } + #[test] fn chain_follow_ids_are_scoped_per_product_core() { let (host_config, product) = runtime_config("same.dot"); diff --git a/rust/crates/truapi-server/src/test_support.rs b/rust/crates/truapi-server/src/test_support.rs index a9331624d..38cbf9d17 100644 --- a/rust/crates/truapi-server/src/test_support.rs +++ b/rust/crates/truapi-server/src/test_support.rs @@ -890,6 +890,16 @@ impl PlatformFeatures for StubPlatform { ) -> Result { Ok(v01::HostFeatureSupportedResponse { supported: true }) } + + async fn supported_chains(&self) -> Result { + Ok(truapi_platform::HostChainSet { + network: "paseo".to_string(), + chains: vec![truapi_platform::HostChainEntry { + identifier: v01::ChainIdentifier::AssetHub, + genesis_hash: [0xaa; 32], + }], + }) + } } struct RecordingConnection { diff --git a/rust/crates/truapi-server/src/wasm/generated_bridge.rs b/rust/crates/truapi-server/src/wasm/generated_bridge.rs index c08a48688..322f3068e 100644 --- a/rust/crates/truapi-server/src/wasm/generated_bridge.rs +++ b/rust/crates/truapi-server/src/wasm/generated_bridge.rs @@ -27,6 +27,7 @@ pub(super) struct JsBridge { pub(super) write_core_storage: Function, pub(super) clear_core_storage: Function, pub(super) feature_supported: Function, + pub(super) supported_chains: Function, pub(super) navigate_to: Function, pub(super) push_notification: Function, pub(super) cancel_notification: Function, @@ -49,6 +50,7 @@ impl JsBridge { write_core_storage: get_function(callbacks, "writeCoreStorage")?, clear_core_storage: get_function(callbacks, "clearCoreStorage")?, feature_supported: get_function(callbacks, "featureSupported")?, + supported_chains: get_function(callbacks, "supportedChains")?, navigate_to: get_function(callbacks, "navigateTo")?, push_notification: get_function(callbacks, "pushNotification")?, cancel_notification: get_function(callbacks, "cancelNotification")?, @@ -137,6 +139,17 @@ impl truapi_platform::Features for WasmPlatform { ) .map_err(generic) } + + async fn supported_chains(&self) -> Result { + let bytes = invoke_bytes_return(&self.bridge.supported_chains, Vec::new()) + .await + .map_err(generic)?; + decode_bytes::( + bytes, + "supportedChains response did not decode", + ) + .map_err(generic) + } } #[truapi_platform::async_trait] diff --git a/rust/crates/truapi-server/tests/common/mod.rs b/rust/crates/truapi-server/tests/common/mod.rs index be868eaf2..e723896a1 100644 --- a/rust/crates/truapi-server/tests/common/mod.rs +++ b/rust/crates/truapi-server/tests/common/mod.rs @@ -131,6 +131,16 @@ impl Features for WireShapePlatform { ) -> Result { Ok(v01::HostFeatureSupportedResponse { supported: true }) } + + async fn supported_chains(&self) -> Result { + Ok(truapi_platform::HostChainSet { + network: "paseo".to_string(), + chains: vec![truapi_platform::HostChainEntry { + identifier: v01::ChainIdentifier::AssetHub, + genesis_hash: [0xaa; 32], + }], + }) + } } struct DeadConnection; diff --git a/rust/crates/truapi-server/tests/wire_result_shape.rs b/rust/crates/truapi-server/tests/wire_result_shape.rs index 4ce821934..6fc32194a 100644 --- a/rust/crates/truapi-server/tests/wire_result_shape.rs +++ b/rust/crates/truapi-server/tests/wire_result_shape.rs @@ -66,6 +66,63 @@ fn feature_supported_ok_response_uses_ok_discriminant() { assert_eq!(response.payload.value.get(1), Some(&0x00)); } +#[test] +fn get_chain_info_ok_response_round_trips_over_the_wire() { + let core = make_core(); + let request = + truapi::versioned::chain::RemoteChainInfoRequest::V1(v01::RemoteChainInfoRequest { + chain: v01::ChainIdentifier::AssetHub, + }); + let ids = request_ids("chain_get_chain_info").expect("known request method"); + let frame = ProtocolMessage { + request_id: "p:9".into(), + payload: Payload { + id: ids.request_id, + value: request.encode(), + }, + }; + let response = dispatch(&core, frame); + assert_eq!(response.request_id, "p:9"); + assert_eq!(response.payload.id, ids.response_id); + + // Wire payload: [V1 disc=0x00][Ok disc=0x00][encoded response body]. + let mut expected = vec![0x00u8, 0x00u8]; + v01::RemoteChainInfoResponse { + network: "paseo".to_string(), + chain: v01::ChainIdentifier::AssetHub, + genesis_hash: [0xaa; 32], + } + .encode_to(&mut expected); + assert_eq!(response.payload.value, expected); +} + +#[test] +fn get_chain_info_unserved_chain_uses_err_discriminant() { + let core = make_core(); + let request = + truapi::versioned::chain::RemoteChainInfoRequest::V1(v01::RemoteChainInfoRequest { + chain: v01::ChainIdentifier::Bulletin, + }); + let ids = request_ids("chain_get_chain_info").expect("known request method"); + let frame = ProtocolMessage { + request_id: "p:10".into(), + payload: Payload { + id: ids.request_id, + value: request.encode(), + }, + }; + let response = dispatch(&core, frame); + assert_eq!(response.payload.id, ids.response_id); + + // Wire payload: [V1 disc=0x00][Err disc=0x01][encoded domain error]. + let mut expected = vec![0x00u8, 0x01u8]; + CallError::Domain(truapi::versioned::chain::RemoteChainInfoError::V1( + v01::RemoteChainInfoError::NotSupported, + )) + .encode_to(&mut expected); + assert_eq!(response.payload.value, expected); +} + #[test] fn local_storage_read_err_response_uses_err_discriminant() { let core = make_core(); diff --git a/rust/crates/truapi/src/lib.rs b/rust/crates/truapi/src/lib.rs index a138df037..0273636fa 100644 --- a/rust/crates/truapi/src/lib.rs +++ b/rust/crates/truapi/src/lib.rs @@ -31,8 +31,8 @@ pub mod latest { use crate::versioned::{self, Versioned}; pub use crate::v01::{ - AccountId, AllocatableResource, AllocationOutcome, ContextualAlias, DerivationIndex, - GenericError, HostSignPayloadData, NotificationId, OperationStartedResult, + AccountId, AllocatableResource, AllocationOutcome, ChainIdentifier, ContextualAlias, + DerivationIndex, GenericError, HostSignPayloadData, NotificationId, OperationStartedResult, ProductAccountId, ProductProofContext, RawPayload, RemotePermission, RemoteStatementStoreCreateProofError, RemoteStatementStoreCreateProofRequest, RemoteStatementStoreCreateProofResponse, RemoteStatementStoreSubscribeItem, @@ -109,6 +109,12 @@ pub mod latest { pub type ProductAccountTxPayload = LatestOf; /// Chain-head subscription item. pub type RemoteChainHeadFollowItem = LatestOf; + /// Chain-identifier resolution error. + pub type RemoteChainInfoError = LatestOf; + /// Chain-identifier resolution request. + pub type RemoteChainInfoRequest = LatestOf; + /// Chain-identifier resolution result. + pub type RemoteChainInfoResponse = LatestOf; /// Chain-head subscription request. pub type RemoteChainHeadFollowRequest = LatestOf; From ff4d8a809b494250345f2089b4ea9eb8c8c6d5ae Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Mon, 10 Aug 2026 11:27:52 -0300 Subject: [PATCH 7/8] regenerate UniFFI Swift bindings for supportedChains --- .../Sources/TrUAPIHost/truapi.swift | 95 ++++++++++ .../Sources/TrUAPIHost/truapi_platform.swift | 164 ++++++++++++++++++ .../Sources/TrUAPIHost/truapi_server.swift | 78 ++++++++- .../include/truapi_serverFFI.h | 32 +++- 4 files changed, 359 insertions(+), 10 deletions(-) diff --git a/ios/truapi-host/Sources/TrUAPIHost/truapi.swift b/ios/truapi-host/Sources/TrUAPIHost/truapi.swift index f889b39d0..d1dd8b802 100644 --- a/ios/truapi-host/Sources/TrUAPIHost/truapi.swift +++ b/ios/truapi-host/Sources/TrUAPIHost/truapi.swift @@ -3688,6 +3688,101 @@ public func FfiConverterTypeButtonVariant_lower(_ value: ButtonVariant) -> RustB +/** + * Role of a chain within the host's configured environment. + */ + +public enum ChainIdentifier: Equatable, Hashable { + + /** + * The relay chain. + */ + case relay + /** + * The asset hub system chain. + */ + case assetHub + /** + * The people chain. + */ + case people + /** + * The bulletin chain. + */ + case bulletin + + + + + +} + +#if compiler(>=6) +extension ChainIdentifier: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeChainIdentifier: FfiConverterRustBuffer { + typealias SwiftType = ChainIdentifier + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ChainIdentifier { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .relay + + case 2: return .assetHub + + case 3: return .people + + case 4: return .bulletin + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: ChainIdentifier, into buf: inout [UInt8]) { + switch value { + + + case .relay: + writeInt(&buf, Int32(1)) + + + case .assetHub: + writeInt(&buf, Int32(2)) + + + case .people: + writeInt(&buf, Int32(3)) + + + case .bulletin: + writeInt(&buf, Int32(4)) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeChainIdentifier_lift(_ buf: RustBuffer) throws -> ChainIdentifier { + return try FfiConverterTypeChainIdentifier.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeChainIdentifier_lower(_ value: ChainIdentifier) -> RustBuffer { + return FfiConverterTypeChainIdentifier.lower(value) +} + + + /** * Layout for action buttons. */ diff --git a/ios/truapi-host/Sources/TrUAPIHost/truapi_platform.swift b/ios/truapi-host/Sources/TrUAPIHost/truapi_platform.swift index d2aace538..d15683af4 100644 --- a/ios/truapi-host/Sources/TrUAPIHost/truapi_platform.swift +++ b/ios/truapi-host/Sources/TrUAPIHost/truapi_platform.swift @@ -780,6 +780,145 @@ public func FfiConverterTypeCreateProofReview_lower(_ value: CreateProofReview) } +/** + * One chain a host serves: a protocol chain role mapped to the concrete + * chain of the host's configured environment. + */ +public struct HostChainEntry: Equatable, Hashable { + /** + * Protocol role this entry answers for. + */ + public var identifier: ChainIdentifier + /** + * Genesis hash identifying the chain in all chain-scoped calls. + */ + public var genesisHash: Bytes32 + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init( + /** + * Protocol role this entry answers for. + */identifier: ChainIdentifier, + /** + * Genesis hash identifying the chain in all chain-scoped calls. + */genesisHash: Bytes32) { + self.identifier = identifier + self.genesisHash = genesisHash + } + + + + +} + +#if compiler(>=6) +extension HostChainEntry: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeHostChainEntry: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> HostChainEntry { + return + try HostChainEntry( + identifier: FfiConverterTypeChainIdentifier.read(from: &buf), + genesisHash: FfiConverterTypeBytes32.read(from: &buf) + ) + } + + public static func write(_ value: HostChainEntry, into buf: inout [UInt8]) { + FfiConverterTypeChainIdentifier.write(value.identifier, into: &buf) + FfiConverterTypeBytes32.write(value.genesisHash, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeHostChainEntry_lift(_ buf: RustBuffer) throws -> HostChainEntry { + return try FfiConverterTypeHostChainEntry.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeHostChainEntry_lower(_ value: HostChainEntry) -> RustBuffer { + return FfiConverterTypeHostChainEntry.lower(value) +} + + +/** + * The chain set a host serves: its environment plus one entry per chain role. + */ +public struct HostChainSet: Equatable, Hashable { + /** + * Ecosystem the host is configured for, e.g. "polkadot", "paseo". + */ + public var network: String + /** + * Complete set of chains available through this host. + */ + public var chains: [HostChainEntry] + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init( + /** + * Ecosystem the host is configured for, e.g. "polkadot", "paseo". + */network: String, + /** + * Complete set of chains available through this host. + */chains: [HostChainEntry]) { + self.network = network + self.chains = chains + } + + + + +} + +#if compiler(>=6) +extension HostChainSet: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeHostChainSet: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> HostChainSet { + return + try HostChainSet( + network: FfiConverterString.read(from: &buf), + chains: FfiConverterSequenceTypeHostChainEntry.read(from: &buf) + ) + } + + public static func write(_ value: HostChainSet, into buf: inout [UInt8]) { + FfiConverterString.write(value.network, into: &buf) + FfiConverterSequenceTypeHostChainEntry.write(value.chains, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeHostChainSet_lift(_ buf: RustBuffer) throws -> HostChainSet { + return try FfiConverterTypeHostChainSet.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeHostChainSet_lower(_ value: HostChainSet) -> RustBuffer { + return FfiConverterTypeHostChainSet.lower(value) +} + + /** * Review shown before a product learns the user's primary identity. */ @@ -2085,6 +2224,31 @@ fileprivate struct FfiConverterOptionTypeBytes32: FfiConverterRustBuffer { } } +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterSequenceTypeHostChainEntry: FfiConverterRustBuffer { + typealias SwiftType = [HostChainEntry] + + public static func write(_ value: [HostChainEntry], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeHostChainEntry.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [HostChainEntry] { + let len: Int32 = try readInt(&buf) + var seq = [HostChainEntry]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeHostChainEntry.read(from: &buf)) + } + return seq + } +} + #if swift(>=5.8) @_documentation(visibility: private) #endif diff --git a/ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift b/ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift index 51ddd36ac..1fb5581c2 100644 --- a/ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift +++ b/ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift @@ -723,6 +723,13 @@ public protocol HostCallbacks: AnyObject, Sendable { */ func featureSupported(request: HostFeatureSupportedRequest) async throws -> Bool + /** + * Enumerate the chains this host serves (RFC 0026): its environment plus + * one entry per chain role. The returned set must match exactly what + * `chain_connect` will accept. + */ + func supportedChains() async throws -> HostChainSet + /** * Read a value from the host's scoped key-value store. */ @@ -1079,6 +1086,27 @@ open func featureSupported(request: HostFeatureSupportedRequest)async throws -> ) } + /** + * Enumerate the chains this host serves (RFC 0026): its environment plus + * one entry per chain role. The returned set must match exactly what + * `chain_connect` will accept. + */ +open func supportedChains()async throws -> HostChainSet { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_truapi_server_fn_method_hostcallbacks_supported_chains( + self.uniffiCloneHandle() + ) + }, + pollFunc: ffi_truapi_server_rust_future_poll_rust_buffer, + completeFunc: ffi_truapi_server_rust_future_complete_rust_buffer, + freeFunc: ffi_truapi_server_rust_future_free_rust_buffer, + liftFunc: FfiConverterTypeHostChainSet_lift, + errorHandler: FfiConverterTypeHostRejection_lift + ) +} + /** * Read a value from the host's scoped key-value store. */ @@ -1696,6 +1724,47 @@ fileprivate struct UniffiCallbackInterfaceHostCallbacks { droppedCallback: uniffiOutDroppedCallback ) }, + supportedChains: { ( + uniffiHandle: UInt64, + uniffiFutureCallback: @escaping UniffiForeignFutureCompleteRustBuffer, + uniffiCallbackData: UInt64, + uniffiOutDroppedCallback: UnsafeMutablePointer + ) in + let makeCall = { + () async throws -> HostChainSet in + guard let uniffiObj = try? FfiConverterTypeHostCallbacks.handleMap.get(handle: uniffiHandle) else { + throw UniffiInternalError.unexpectedStaleHandle + } + return try await uniffiObj.supportedChains( + ) + } + + let uniffiHandleSuccess = { (returnValue: HostChainSet) in + uniffiFutureCallback( + uniffiCallbackData, + UniffiForeignFutureResultRustBuffer( + returnValue: FfiConverterTypeHostChainSet_lower(returnValue), + callStatus: RustCallStatus() + ) + ) + } + let uniffiHandleError = { (statusCode, errorBuf) in + uniffiFutureCallback( + uniffiCallbackData, + UniffiForeignFutureResultRustBuffer( + returnValue: RustBuffer.empty(), + callStatus: RustCallStatus(code: statusCode, errorBuf: errorBuf) + ) + ) + } + uniffiTraitInterfaceCallAsyncWithError( + makeCall: makeCall, + handleSuccess: uniffiHandleSuccess, + handleError: uniffiHandleError, + lowerError: FfiConverterTypeHostRejection_lower, + droppedCallback: uniffiOutDroppedCallback + ) + }, localStorageRead: { ( uniffiHandle: UInt64, key: RustBuffer, @@ -5173,13 +5242,16 @@ private let initializationResult: InitializationResult = { if (uniffi_truapi_server_checksum_method_hostcallbacks_feature_supported() != 46490) { return InitializationResult.apiChecksumMismatch } - if (uniffi_truapi_server_checksum_method_hostcallbacks_local_storage_read() != 54709) { + if (uniffi_truapi_server_checksum_method_hostcallbacks_supported_chains() != 26390) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_truapi_server_checksum_method_hostcallbacks_local_storage_read() != 32804) { return InitializationResult.apiChecksumMismatch } - if (uniffi_truapi_server_checksum_method_hostcallbacks_local_storage_write() != 33044) { + if (uniffi_truapi_server_checksum_method_hostcallbacks_local_storage_write() != 62222) { return InitializationResult.apiChecksumMismatch } - if (uniffi_truapi_server_checksum_method_hostcallbacks_local_storage_clear() != 6971) { + if (uniffi_truapi_server_checksum_method_hostcallbacks_local_storage_clear() != 61208) { return InitializationResult.apiChecksumMismatch } if (uniffi_truapi_server_checksum_method_nativechatcallbacks_create_room() != 15676) { diff --git a/ios/truapi-host/Sources/truapi_serverFFI/include/truapi_serverFFI.h b/ios/truapi-host/Sources/truapi_serverFFI/include/truapi_serverFFI.h index e867eb575..62c7fa137 100644 --- a/ios/truapi-host/Sources/truapi_serverFFI/include/truapi_serverFFI.h +++ b/ios/truapi-host/Sources/truapi_serverFFI/include/truapi_serverFFI.h @@ -371,21 +371,27 @@ typedef void (*UniffiCallbackInterfaceHostCallbacksMethod16)(uint64_t, RustBuffe #endif #ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_HOST_CALLBACKS_METHOD17 #define UNIFFI_FFIDEF_CALLBACK_INTERFACE_HOST_CALLBACKS_METHOD17 -typedef void (*UniffiCallbackInterfaceHostCallbacksMethod17)(uint64_t, RustBuffer, RustBuffer* _Nonnull, - RustCallStatus *_Nonnull uniffiCallStatus +typedef void (*UniffiCallbackInterfaceHostCallbacksMethod17)(uint64_t, UniffiForeignFutureCompleteRustBuffer _Nonnull, uint64_t, UniffiForeignFutureDroppedCallbackStruct* _Nonnull ); #endif #ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_HOST_CALLBACKS_METHOD18 #define UNIFFI_FFIDEF_CALLBACK_INTERFACE_HOST_CALLBACKS_METHOD18 -typedef void (*UniffiCallbackInterfaceHostCallbacksMethod18)(uint64_t, RustBuffer, RustBuffer, void* _Nonnull, +typedef void (*UniffiCallbackInterfaceHostCallbacksMethod18)(uint64_t, RustBuffer, RustBuffer* _Nonnull, RustCallStatus *_Nonnull uniffiCallStatus ); #endif #ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_HOST_CALLBACKS_METHOD19 #define UNIFFI_FFIDEF_CALLBACK_INTERFACE_HOST_CALLBACKS_METHOD19 -typedef void (*UniffiCallbackInterfaceHostCallbacksMethod19)(uint64_t, RustBuffer, void* _Nonnull, +typedef void (*UniffiCallbackInterfaceHostCallbacksMethod19)(uint64_t, RustBuffer, RustBuffer, void* _Nonnull, + RustCallStatus *_Nonnull uniffiCallStatus + ); + +#endif +#ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_HOST_CALLBACKS_METHOD20 +#define UNIFFI_FFIDEF_CALLBACK_INTERFACE_HOST_CALLBACKS_METHOD20 +typedef void (*UniffiCallbackInterfaceHostCallbacksMethod20)(uint64_t, RustBuffer, void* _Nonnull, RustCallStatus *_Nonnull uniffiCallStatus ); @@ -450,9 +456,10 @@ typedef struct UniffiVTableCallbackInterfaceHostCallbacks { UniffiCallbackInterfaceHostCallbacksMethod14 _Nonnull lookupPreimage; UniffiCallbackInterfaceHostCallbacksMethod15 _Nonnull currentTheme; UniffiCallbackInterfaceHostCallbacksMethod16 _Nonnull featureSupported; - UniffiCallbackInterfaceHostCallbacksMethod17 _Nonnull localStorageRead; - UniffiCallbackInterfaceHostCallbacksMethod18 _Nonnull localStorageWrite; - UniffiCallbackInterfaceHostCallbacksMethod19 _Nonnull localStorageClear; + UniffiCallbackInterfaceHostCallbacksMethod17 _Nonnull supportedChains; + UniffiCallbackInterfaceHostCallbacksMethod18 _Nonnull localStorageRead; + UniffiCallbackInterfaceHostCallbacksMethod19 _Nonnull localStorageWrite; + UniffiCallbackInterfaceHostCallbacksMethod20 _Nonnull localStorageClear; } UniffiVTableCallbackInterfaceHostCallbacks; #endif @@ -568,6 +575,11 @@ RustBuffer uniffi_truapi_server_fn_method_hostcallbacks_current_theme(uint64_t p uint64_t uniffi_truapi_server_fn_method_hostcallbacks_feature_supported(uint64_t ptr, RustBuffer request ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_HOSTCALLBACKS_SUPPORTED_CHAINS +#define UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_HOSTCALLBACKS_SUPPORTED_CHAINS +uint64_t uniffi_truapi_server_fn_method_hostcallbacks_supported_chains(uint64_t ptr +); +#endif #ifndef UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_HOSTCALLBACKS_LOCAL_STORAGE_READ #define UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_HOSTCALLBACKS_LOCAL_STORAGE_READ RustBuffer uniffi_truapi_server_fn_method_hostcallbacks_local_storage_read(uint64_t ptr, RustBuffer key, RustCallStatus *_Nonnull out_status @@ -1205,6 +1217,12 @@ uint16_t uniffi_truapi_server_checksum_method_hostcallbacks_current_theme(void #define UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_CHECKSUM_METHOD_HOSTCALLBACKS_FEATURE_SUPPORTED uint16_t uniffi_truapi_server_checksum_method_hostcallbacks_feature_supported(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_CHECKSUM_METHOD_HOSTCALLBACKS_SUPPORTED_CHAINS +#define UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_CHECKSUM_METHOD_HOSTCALLBACKS_SUPPORTED_CHAINS +uint16_t uniffi_truapi_server_checksum_method_hostcallbacks_supported_chains(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_CHECKSUM_METHOD_HOSTCALLBACKS_LOCAL_STORAGE_READ From c56b8222a4f7ee5e759d1171e5bdd118ca595e0d Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Mon, 10 Aug 2026 11:53:32 -0300 Subject: [PATCH 8/8] wire supportedChains through the Swift and Kotlin bridges as a sync callback --- .../kotlin/io/parity/truapi/TrUAPIHost.kt | 11 +++ .../Sources/TrUAPIHost/TrUAPIHost.swift | 12 ++++ .../Sources/TrUAPIHost/truapi_server.swift | 69 +++++++------------ .../include/truapi_serverFFI.h | 5 +- rust/crates/truapi-server/src/native.rs | 16 ++--- 5 files changed, 55 insertions(+), 58 deletions(-) diff --git a/android/truapi-host/src/main/kotlin/io/parity/truapi/TrUAPIHost.kt b/android/truapi-host/src/main/kotlin/io/parity/truapi/TrUAPIHost.kt index 8b1704e54..91ac870fe 100644 --- a/android/truapi-host/src/main/kotlin/io/parity/truapi/TrUAPIHost.kt +++ b/android/truapi-host/src/main/kotlin/io/parity/truapi/TrUAPIHost.kt @@ -32,6 +32,7 @@ import uniffi.truapi.HostPushNotificationRequest import uniffi.truapi.RemotePermission import uniffi.truapi.ThemeVariant import uniffi.truapi_platform.AuthState +import uniffi.truapi_platform.HostChainSet import uniffi.truapi_platform.PermissionAuthorizationRequest import uniffi.truapi_platform.PermissionAuthorizationStatus import uniffi.truapi_platform.UserConfirmationReview @@ -302,6 +303,13 @@ interface HostBridge { @Throws(HostRejection::class) suspend fun featureSupported(request: HostFeatureSupportedRequest): Boolean + /** + * Enumerate the chains this host serves: its environment plus one entry + * per chain role. Must match exactly what [chainConnect] accepts. + */ + @Throws(HostRejection::class) + fun supportedChains(): HostChainSet = HostChainSet(network = "", chains = emptyList()) + /** Product-scoped key-value storage for the Rust core. */ val storage: HostStorage @@ -366,6 +374,9 @@ private class HostCallbackAdapter(private val bridge: HostBridge) : HostCallback override suspend fun featureSupported(request: HostFeatureSupportedRequest): Boolean = bridge.featureSupported(request) + override fun supportedChains(): HostChainSet = + bridge.supportedChains() + override fun localStorageRead(key: String): ByteArray? = bridge.storage.read(key) diff --git a/ios/truapi-host/Sources/TrUAPIHost/TrUAPIHost.swift b/ios/truapi-host/Sources/TrUAPIHost/TrUAPIHost.swift index 71182712a..0add372c7 100644 --- a/ios/truapi-host/Sources/TrUAPIHost/TrUAPIHost.swift +++ b/ios/truapi-host/Sources/TrUAPIHost/TrUAPIHost.swift @@ -402,6 +402,11 @@ public protocol HostBridge: AnyObject, Sendable { /// return promptly. func featureSupported(request: HostFeatureSupportedRequest) async throws -> Bool + /// Enumerate the chains this host serves: its environment plus one entry + /// per chain role. Must match exactly what ``chainConnect(genesisHash:)`` + /// accepts. Invoked on the dispatcher thread; must return promptly. + func supportedChains() throws -> HostChainSet + /// Scoped key-value storage for the Rust core. var storage: HostStorageBackend { get } @@ -445,6 +450,7 @@ public extension HostBridge { func confirmUserAction(review: UserConfirmationReview) async throws -> Bool { false } func lookupPreimage(key: Data) async throws -> Data? { nil } func currentTheme() throws -> ThemeVariant { .dark } + func supportedChains() throws -> HostChainSet { HostChainSet(network: "", chains: []) } } /// Adapter that bridges the public `ChatHostBridge` to the generated UniFFI @@ -609,6 +615,12 @@ private final class HostCallbackAdapter: HostCallbacks, @unchecked Sendable { } } + func supportedChains() throws -> HostChainSet { + try withHostRejection { + try bridge.supportedChains() + } + } + func localStorageRead(key: String) throws -> Data? { try withStorageError { try bridge.storage.read(key: key) diff --git a/ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift b/ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift index 1fb5581c2..171ea91ff 100644 --- a/ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift +++ b/ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift @@ -726,9 +726,10 @@ public protocol HostCallbacks: AnyObject, Sendable { /** * Enumerate the chains this host serves (RFC 0026): its environment plus * one entry per chain role. The returned set must match exactly what - * `chain_connect` will accept. + * `chain_connect` will accept. Invoked on the dispatcher thread; must + * return promptly. */ - func supportedChains() async throws -> HostChainSet + func supportedChains() throws -> HostChainSet /** * Read a value from the host's scoped key-value store. @@ -1089,22 +1090,16 @@ open func featureSupported(request: HostFeatureSupportedRequest)async throws -> /** * Enumerate the chains this host serves (RFC 0026): its environment plus * one entry per chain role. The returned set must match exactly what - * `chain_connect` will accept. + * `chain_connect` will accept. Invoked on the dispatcher thread; must + * return promptly. */ -open func supportedChains()async throws -> HostChainSet { - return - try await uniffiRustCallAsync( - rustFutureFunc: { - uniffi_truapi_server_fn_method_hostcallbacks_supported_chains( - self.uniffiCloneHandle() - ) - }, - pollFunc: ffi_truapi_server_rust_future_poll_rust_buffer, - completeFunc: ffi_truapi_server_rust_future_complete_rust_buffer, - freeFunc: ffi_truapi_server_rust_future_free_rust_buffer, - liftFunc: FfiConverterTypeHostChainSet_lift, - errorHandler: FfiConverterTypeHostRejection_lift - ) +open func supportedChains()throws -> HostChainSet { + return try FfiConverterTypeHostChainSet_lift(try rustCallWithError(FfiConverterTypeHostRejection_lift) { + uniffiCallStatus in + uniffi_truapi_server_fn_method_hostcallbacks_supported_chains( + self.uniffiCloneHandle(),uniffiCallStatus + ) +}) } /** @@ -1726,43 +1721,25 @@ fileprivate struct UniffiCallbackInterfaceHostCallbacks { }, supportedChains: { ( uniffiHandle: UInt64, - uniffiFutureCallback: @escaping UniffiForeignFutureCompleteRustBuffer, - uniffiCallbackData: UInt64, - uniffiOutDroppedCallback: UnsafeMutablePointer + uniffiOutReturn: UnsafeMutablePointer, + uniffiCallStatus: UnsafeMutablePointer ) in let makeCall = { - () async throws -> HostChainSet in + () throws -> HostChainSet in guard let uniffiObj = try? FfiConverterTypeHostCallbacks.handleMap.get(handle: uniffiHandle) else { throw UniffiInternalError.unexpectedStaleHandle } - return try await uniffiObj.supportedChains( + return try uniffiObj.supportedChains( ) } - let uniffiHandleSuccess = { (returnValue: HostChainSet) in - uniffiFutureCallback( - uniffiCallbackData, - UniffiForeignFutureResultRustBuffer( - returnValue: FfiConverterTypeHostChainSet_lower(returnValue), - callStatus: RustCallStatus() - ) - ) - } - let uniffiHandleError = { (statusCode, errorBuf) in - uniffiFutureCallback( - uniffiCallbackData, - UniffiForeignFutureResultRustBuffer( - returnValue: RustBuffer.empty(), - callStatus: RustCallStatus(code: statusCode, errorBuf: errorBuf) - ) - ) - } - uniffiTraitInterfaceCallAsyncWithError( + + let writeReturn = { uniffiOutReturn.pointee = FfiConverterTypeHostChainSet_lower($0) } + uniffiTraitInterfaceCallWithError( + callStatus: uniffiCallStatus, makeCall: makeCall, - handleSuccess: uniffiHandleSuccess, - handleError: uniffiHandleError, - lowerError: FfiConverterTypeHostRejection_lower, - droppedCallback: uniffiOutDroppedCallback + writeReturn: writeReturn, + lowerError: FfiConverterTypeHostRejection_lower ) }, localStorageRead: { ( @@ -5242,7 +5219,7 @@ private let initializationResult: InitializationResult = { if (uniffi_truapi_server_checksum_method_hostcallbacks_feature_supported() != 46490) { return InitializationResult.apiChecksumMismatch } - if (uniffi_truapi_server_checksum_method_hostcallbacks_supported_chains() != 26390) { + if (uniffi_truapi_server_checksum_method_hostcallbacks_supported_chains() != 23356) { return InitializationResult.apiChecksumMismatch } if (uniffi_truapi_server_checksum_method_hostcallbacks_local_storage_read() != 32804) { diff --git a/ios/truapi-host/Sources/truapi_serverFFI/include/truapi_serverFFI.h b/ios/truapi-host/Sources/truapi_serverFFI/include/truapi_serverFFI.h index 62c7fa137..40224ac8a 100644 --- a/ios/truapi-host/Sources/truapi_serverFFI/include/truapi_serverFFI.h +++ b/ios/truapi-host/Sources/truapi_serverFFI/include/truapi_serverFFI.h @@ -371,7 +371,8 @@ typedef void (*UniffiCallbackInterfaceHostCallbacksMethod16)(uint64_t, RustBuffe #endif #ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_HOST_CALLBACKS_METHOD17 #define UNIFFI_FFIDEF_CALLBACK_INTERFACE_HOST_CALLBACKS_METHOD17 -typedef void (*UniffiCallbackInterfaceHostCallbacksMethod17)(uint64_t, UniffiForeignFutureCompleteRustBuffer _Nonnull, uint64_t, UniffiForeignFutureDroppedCallbackStruct* _Nonnull +typedef void (*UniffiCallbackInterfaceHostCallbacksMethod17)(uint64_t, RustBuffer* _Nonnull, + RustCallStatus *_Nonnull uniffiCallStatus ); #endif @@ -577,7 +578,7 @@ uint64_t uniffi_truapi_server_fn_method_hostcallbacks_feature_supported(uint64_t #endif #ifndef UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_HOSTCALLBACKS_SUPPORTED_CHAINS #define UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_HOSTCALLBACKS_SUPPORTED_CHAINS -uint64_t uniffi_truapi_server_fn_method_hostcallbacks_supported_chains(uint64_t ptr +RustBuffer uniffi_truapi_server_fn_method_hostcallbacks_supported_chains(uint64_t ptr, RustCallStatus *_Nonnull out_status ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_TRUAPI_SERVER_FN_METHOD_HOSTCALLBACKS_LOCAL_STORAGE_READ diff --git a/rust/crates/truapi-server/src/native.rs b/rust/crates/truapi-server/src/native.rs index 7cc9bf062..4352ec71e 100644 --- a/rust/crates/truapi-server/src/native.rs +++ b/rust/crates/truapi-server/src/native.rs @@ -468,8 +468,9 @@ pub trait HostCallbacks: Send + Sync { /// Enumerate the chains this host serves (RFC 0026): its environment plus /// one entry per chain role. The returned set must match exactly what - /// `chain_connect` will accept. - async fn supported_chains(&self) -> Result; + /// `chain_connect` will accept. Invoked on the dispatcher thread; must + /// return promptly. + fn supported_chains(&self) -> Result; /// Read a value from the host's scoped key-value store. fn local_storage_read(&self, key: String) -> Result>, HostStorageError>; @@ -1303,7 +1304,6 @@ impl Features for CallbackPlatform { self.callbacks .supported_chains() - .await .map_err(v01::GenericError::from) } } @@ -1696,7 +1696,7 @@ mod tests { ) -> Result { Ok(false) } - async fn supported_chains(&self) -> Result { + fn supported_chains(&self) -> Result { Ok(truapi_platform::HostChainSet { network: "paseo".to_string(), chains: Vec::new(), @@ -2326,9 +2326,7 @@ mod tests { ) -> Result { Ok(false) } - async fn supported_chains( - &self, - ) -> Result { + fn supported_chains(&self) -> Result { Ok(truapi_platform::HostChainSet { network: "paseo".to_string(), chains: Vec::new(), @@ -2473,9 +2471,7 @@ mod tests { ) -> Result { Ok(true) } - async fn supported_chains( - &self, - ) -> Result { + fn supported_chains(&self) -> Result { Ok(truapi_platform::HostChainSet { network: "paseo".to_string(), chains: Vec::new(),