From c94a07b42fb73354ce96d180b8c86a256c6bd49c Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Thu, 6 Aug 2026 14:00:44 -0300 Subject: [PATCH 1/5] 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/5] 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/5] 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/5] 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/5] 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), }