From 14e0b23233198d5b881f159016a48eae182c7648 Mon Sep 17 00:00:00 2001 From: Mikelle Date: Fri, 6 Nov 2026 13:20:56 +0100 Subject: [PATCH 1/3] feat(truapi)!: take chain config from the Trinity stack Hosts pass the stack's pointer, the signed descriptor their build bundles, and whether the build allows Polkadot's reserved values, instead of genesis hashes, a network suffix and a supportedChains callback. - truapi: Stack::bundled verifies the bundled copy against the pointer's key and refuses a pointer without one, a descriptor without People or Bulletin, a TLD the core does not serve, and Polkadot's genesis hashes or `dot` unless the build allows them. Stack::new builds one from parts for Rust hosts under the same rules. - get_chain_info, the People, Bulletin and Asset Hub chains and the signing role's TLD come from the stack. Features::supported_chains, HostChainSet and HostChainEntry are gone. - Native: HostRuntimeConfig.stack (BundledStack). Browser: runtimeConfig.stack, and runtimeConfig.testStack in the test-host bundle. - CLI: builds its stack from the network it resolved; only Polkadot may use reserved values. - iOS and Android apps bundle the previewnet, paseo-next-v2 and Polkadot descriptors and pick one per build. Closes #1881 --- .changeset/stack-chain-config.md | 14 ++ .changeset/test-host-unserved-chains.md | 4 +- AGENTS.md | 4 +- Cargo.lock | 1 + README.md | 6 + android/truapi-host/README.md | 48 ++-- .../kotlin/io/parity/truapi/TrUAPIHost.kt | 11 - docs/rfcs/0022-account-derivations.md | 2 +- .../docs/architecture/host-api-products.md | 29 ++- .../app/MediaPermissionIntegrationTest.kt | 6 +- .../app/RendererLossRecoveryTest.kt | 6 +- .../bindings/truapi-host/build.gradle.kts | 6 +- .../truapi/ContainerHttpAuthorizationTest.kt | 5 +- .../io/parity/truapi/InertHostBridge.kt | 18 +- .../io/parity/truapi/TrUAPIDiagnosticsTest.kt | 11 +- hosts/android/docs/DEPLOYMENT.md | 2 +- .../src/main/assets/stacks/paseo-next-v2.json | 1 + .../impl/src/main/assets/stacks/polkadot.json | 1 + .../src/main/assets/stacks/previewnet.json | 1 + .../domain/truapi/ProductTrUAPIHostBridge.kt | 3 - .../domain/truapi/TrUAPIChainDirectory.kt | 69 +----- .../truapi/TrUAPIHostRuntimeProvider.kt | 28 +-- .../domain/truapi/TrUAPIStack.kt | 50 ++++ .../domain/truapi/TrUAPIChainAgreementTest.kt | 45 +--- .../truapi/TrUAPIHostRuntimeProviderTest.kt | 55 ++++- .../TrUAPI/RustHostRuntimeBridge.swift | 4 - .../TrUAPI/RustProductExecutionBridge.swift | 4 - .../Stacks/trinity-stack-paseo-next-v2.json | 1 + .../TrUAPI/Stacks/trinity-stack-polkadot.json | 1 + .../Stacks/trinity-stack-previewnet.json | 1 + .../Products/TrUAPI/TrUAPIBundledStack.swift | 68 ++++++ .../TrUAPI/TrUAPIHostRuntimeProvider.swift | 48 +--- .../TrUAPI/TrUAPISupportedChains.swift | 43 ---- .../TrUAPI/RustHostRuntimeBridgeTests.swift | 45 +++- ios/truapi-host/README.md | 51 ++--- .../Sources/TrUAPIHost/TrUAPIHost.swift | 11 - .../Tests/ProductNetworkAccessTests.swift | 4 +- .../Tests/TrUAPIWsBridgeTests.swift | 26 ++- js/packages/truapi-host/README.md | 30 +-- js/packages/truapi-host/src/runtime.ts | 56 ++--- js/packages/truapi-host/src/test-support.ts | 21 +- js/packages/truapi-host/src/testing.ts | 4 + .../truapi-host/src/testing/dev-accounts.ts | 23 +- .../src/testing/fixture-compat.test.ts | 54 +++-- .../truapi-host/src/testing/networks.ts | 49 ++-- .../truapi-host/src/testing/playwright.ts | 4 +- .../src/testing/server-config.test.ts | 14 +- .../src/web/create-mock-host.test.ts | 25 +- .../truapi-host/src/web/create-mock-host.ts | 101 +++++--- .../truapi-host/src/web/wasm-bridge.test.ts | 40 +++- .../src/web/worker-test-harness.ts | 15 +- .../tests/golden/host-callbacks-adapter.ts | 4 - .../tests/golden/host-callbacks.ts | 61 ----- .../tests/golden/wasm_bridge.rs | 12 - .../tests/golden/worker-callbacks.ts | 6 - rust/crates/truapi-host-cli/README.md | 2 +- rust/crates/truapi-host-cli/SPEC.md | 2 +- .../crates/truapi-host-cli/src/chain/tests.rs | 14 +- .../truapi-host-cli/src/frame_server/tests.rs | 10 +- rust/crates/truapi-host-cli/src/main.rs | 9 +- rust/crates/truapi-host-cli/src/network.rs | 56 +++-- .../src/network/local_network/tests.rs | 6 +- .../truapi-host-cli/src/network/tests.rs | 72 ++++-- rust/crates/truapi-host-cli/src/platform.rs | 16 +- .../truapi-host-cli/src/platform/tests.rs | 40 +--- rust/crates/truapi/Cargo.toml | 2 + rust/crates/truapi/RUNTIME.md | 28 ++- rust/crates/truapi/src/host_core.rs | 12 +- rust/crates/truapi/src/host_core/tests.rs | 55 ++--- rust/crates/truapi/src/host_logic/features.rs | 91 +------- .../truapi/src/host_logic/features/tests.rs | 48 ---- .../truapi/src/host_logic/product_account.rs | 2 +- .../src/host_logic/sso/pairing/tests.rs | 4 +- rust/crates/truapi/src/image_loader/tests.rs | 4 +- rust/crates/truapi/src/lib.rs | 2 + rust/crates/truapi/src/markers.rs | 3 - rust/crates/truapi/src/native.rs | 4 +- rust/crates/truapi/src/native/callbacks.rs | 5 - rust/crates/truapi/src/native/config.rs | 61 ++--- rust/crates/truapi/src/native/platform.rs | 7 - rust/crates/truapi/src/native/tests.rs | 127 +++++------ .../truapi/src/native/ws_bridge/tests.rs | 5 +- rust/crates/truapi/src/platform.rs | 133 +++-------- rust/crates/truapi/src/platform/mock.rs | 71 +++--- rust/crates/truapi/src/platform/mock/tests.rs | 53 ++--- rust/crates/truapi/src/platform/tests.rs | 42 ---- rust/crates/truapi/src/runtime.rs | 8 +- .../truapi/src/runtime/capabilities/chain.rs | 14 +- .../truapi/src/runtime/pairing_host/tests.rs | 12 +- .../src/runtime/product_manifest/tests.rs | 12 +- rust/crates/truapi/src/runtime/services.rs | 64 ++---- .../crates/truapi/src/runtime/signing_host.rs | 37 +-- .../src/runtime/signing_host/allowance.rs | 20 +- .../truapi/src/runtime/signing_host/tests.rs | 16 +- .../truapi/src/runtime/sso_request_service.rs | 8 +- .../runtime/sso_request_service/pairing.rs | 2 +- .../sso_request_service/pairing/tests.rs | 3 +- .../runtime/sso_responder_service/tests.rs | 12 +- .../src/runtime/statement_store/tests.rs | 6 +- rust/crates/truapi/src/runtime/tests.rs | 112 ++++----- rust/crates/truapi/src/stack.rs | 215 ++++++++++++++++++ rust/crates/truapi/src/stack/tests.rs | 158 +++++++++++++ rust/crates/truapi/src/test_support.rs | 95 ++++++-- rust/crates/truapi/src/truapi_core.rs | 4 +- rust/crates/truapi/src/wasm.rs | 104 ++++++--- rust/crates/truapi/tests/common/mod.rs | 14 +- rust/crates/truapi/tests/platform_bounds.rs | 4 +- .../tests/signing_host_mock_platform.rs | 6 +- .../truapi/tests/wasm_crypto_vectors.rs | 4 +- rust/crates/truapi/tests/wire_result_shape.rs | 7 +- 110 files changed, 1623 insertions(+), 1552 deletions(-) create mode 100644 .changeset/stack-chain-config.md create mode 100644 hosts/android/feature/products/impl/src/main/assets/stacks/paseo-next-v2.json create mode 100644 hosts/android/feature/products/impl/src/main/assets/stacks/polkadot.json create mode 100644 hosts/android/feature/products/impl/src/main/assets/stacks/previewnet.json create mode 100644 hosts/android/feature/products/impl/src/main/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/TrUAPIStack.kt create mode 100644 hosts/ios/polkadot-app/Modules/Products/TrUAPI/Stacks/trinity-stack-paseo-next-v2.json create mode 100644 hosts/ios/polkadot-app/Modules/Products/TrUAPI/Stacks/trinity-stack-polkadot.json create mode 100644 hosts/ios/polkadot-app/Modules/Products/TrUAPI/Stacks/trinity-stack-previewnet.json create mode 100644 hosts/ios/polkadot-app/Modules/Products/TrUAPI/TrUAPIBundledStack.swift delete mode 100644 hosts/ios/polkadot-app/Modules/Products/TrUAPI/TrUAPISupportedChains.swift create mode 100644 rust/crates/truapi/src/stack.rs create mode 100644 rust/crates/truapi/src/stack/tests.rs diff --git a/.changeset/stack-chain-config.md b/.changeset/stack-chain-config.md new file mode 100644 index 0000000000..6d0a607e91 --- /dev/null +++ b/.changeset/stack-chain-config.md @@ -0,0 +1,14 @@ +--- +"@parity/truapi-host": major +--- + +The core takes its chain configuration from a Trinity stack descriptor. `ProductRuntimeConfig.stack` replaces `people`, +`bulletin`, `assetHub` and `networkSuffix`: `pointer` is the stack's link (`polkadotapp://stack?key=0x…&url=…`), +`descriptor` the bytes of the signed copy the host build bundles, and `allowsReservedValues` whether the build may run a +stack on Polkadot's genesis hashes or the `dot` TLD. The core verifies the copy against the pointer's key and refuses a +pointer without a key, a descriptor without a People or Bulletin chain, and a reserved value the build does not allow. +`getChainInfo` answers from the descriptor's `chains` and `network`, so the `supportedChains` host callback and the +`HostChainSet` and `HostChainEntry` types are removed. The test host runs a stack built from its parts: +`mockRuntimeConfig()` returns a `testStack` over the `MOCK_GENESIS` chains, which only the `wasm/testing` bundle reads, +`fromNetworks` and `liveChain` build it from their chains, and `MockHostConfig.servedChains` replaces `supportedChains` +as the genesis hashes `featureSupported` answers from under `onlyServedChains`. diff --git a/.changeset/test-host-unserved-chains.md b/.changeset/test-host-unserved-chains.md index cc79111945..3021f0be56 100644 --- a/.changeset/test-host-unserved-chains.md +++ b/.changeset/test-host-unserved-chains.md @@ -3,6 +3,6 @@ --- The mock host can report chains it does not serve as unsupported. `onlyServedChains: true` answers `featureSupported` -from `supportedChains`, as a real host does, so a product's "network unavailable" path is reachable on a host that -serves no chains. It defaults to `false`, and becomes the default in a later breaking release. The Rust +from `servedChains`, as a real host does, so a product's "network unavailable" path is reachable on a host that serves +no chains. It defaults to `false`, and becomes the default in a later breaking release. The Rust `MockConfig::only_served_chains` is the same option. diff --git a/AGENTS.md b/AGENTS.md index 7664982b5e..7d6ae7c721 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,9 @@ rust/crates/ (canonical), plus the runtime hosts implement (default `runtime` feature); ships as WASM (browser/node); its `platform` module holds the host syscall traits (storage, navigation, consent, ...), - its `chain` module builds the core's own extrinsics through + its `stack` module resolves the Trinity stack the host runs, the + core's only chain configuration, from the descriptor its build + bundles, its `chain` module builds the core's own extrinsics through `ExtrinsicService` and reads and writes contracts through `EvmContractCaller` with signer-bound prepared transactions; on native targets, its `durable` module follows presigned diff --git a/Cargo.lock b/Cargo.lock index 4c9c5a8018..d02f210c60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5958,6 +5958,7 @@ dependencies = [ "tokio-tungstenite", "tracing", "tracing-subscriber", + "trinity-stack", "truapi-macros", "truapi-provider", "truapi-verifiable", diff --git a/README.md b/README.md index 8efa5255d6..ef5f7fba1c 100644 --- a/README.md +++ b/README.md @@ -448,6 +448,12 @@ A stack descriptor describes one deployment of chains and services, as conformance vectors. The plan for it and the tools on top of it is in [docs/design/trinity-stack-library.md](docs/design/trinity-stack-library.md). +The core takes all of its chain configuration from one stack. A host passes the stack's pointer, the signed descriptor +its build bundles, and whether the build may use Polkadot's reserved values: its genesis hashes and the `dot` TLD. The +core verifies the copy against the pointer's key, refuses an unsigned stack and a reserved value the build does not +allow, and answers `get_chain_info` and the signing role's TLD from the descriptor. See +[RUNTIME.md](rust/crates/truapi/RUNTIME.md#stack). + ### Wire debugger [`@parity/truapi-debugger`](js/packages/truapi-debugger) is the consumer for the diff --git a/android/truapi-host/README.md b/android/truapi-host/README.md index ff74b4ee98..0f88d622ff 100644 --- a/android/truapi-host/README.md +++ b/android/truapi-host/README.md @@ -36,20 +36,21 @@ The package is public, so any authenticated GitHub identity can read it. In GitH The consuming app must declare `android.permission.INTERNET` — the localhost WebSocket bridge binds a `127.0.0.1` TCP socket, which requires it even for loopback. -`HostRuntimeConfig.networkSuffix` is required. Supply the bare TLD (`dot`, -`paseo`, or `testnet`) from the same network configuration used by onboarding -and the People/Bulletin genesis hashes. It must match the People chain's -`NetworkSuffix.NetworkSuffix`. Include this configuration update in the -embedding app's package upgrade. - -`HostRuntimeConfig.assetHubChainGenesisHash` is required. Supply the Asset Hub -genesis hash from the same network configuration, as 32 bytes. Product manifests -are read from the dotNS contracts deployed there, so it is what makes a -`trustedProducts` grant resolvable: without a usable value no manifest resolves, -so every cross-product grant not already cached is refused, and the refusal is -indistinguishable from the other product having granted nothing. Pass 32 zero -bytes only to declare deliberately that this host has no Asset Hub. Include this -configuration update in the embedding app's package upgrade. +`HostRuntimeConfig.stack` is required. It is the Trinity stack the build runs, +as a `BundledStack`: + +- `pointer` is the stack's link, `polkadotapp://stack?key=0x…&url=…`. It must + carry a key. +- `descriptor` is the signed descriptor the build bundles, byte for byte as + served. The core verifies it under the pointer's key. +- `allowsReservedValues` is true only in a Polkadot build. Without it the core + refuses a stack that uses Polkadot's genesis hashes or the `dot` TLD. + +The core takes every chain it uses from the descriptor, and answers products' +chain info queries from its `chains` and `network`. The descriptor's TLD is the +one the wallet's reserved identities derive under. An invalid stack makes the +`TrUAPIHostRuntime` constructor throw `NativeRuntimeConfigException.Invalid`. +Include this configuration update in the embedding app's package upgrade. ### Compatibility @@ -130,10 +131,7 @@ val runtime = TrUAPIHostRuntime( bridge = bridge, runtimeConfig = HostRuntimeConfig( hostName = "My Chat Host", - peopleChainGenesisHash = peopleChainGenesisHash, // exactly 32 bytes - bulletinChainGenesisHash = bulletinChainGenesisHash, - assetHubChainGenesisHash = assetHubChainGenesisHash, - networkSuffix = "dot", + stack = bundledStack, ), ) // Chat needs an active session; without one every Chat call answers `Denied`. @@ -307,6 +305,7 @@ import io.parity.truapi.HostBridge import io.parity.truapi.HostCoreStorage import io.parity.truapi.HostStorage import io.parity.truapi.LocalhostBridgeBootstrap +import uniffi.truapi.BundledStack import uniffi.truapi.HostRuntimeConfig import uniffi.truapi.ProductExecutionConfig import uniffi.truapi.ExecutableKind @@ -419,13 +418,12 @@ val bridge = MyBridge(webView) val runtimeConfig = HostRuntimeConfig( hostName = "My Host", hostIcon = "https://host.example/icon.png", - peopleChainGenesisHash = ByteArray(32), - bulletinChainGenesisHash = ByteArray(32), - // Stand-in for a real Asset Hub genesis hash. Non-zero on purpose: - // all-zero is the "no Asset Hub" sentinel and refuses every cross-product - // `trustedProducts` grant. - assetHubChainGenesisHash = ByteArray(32) { 1.toByte() }, - networkSuffix = "dot", + stack = BundledStack( + // The link the bundled descriptor was published under. + pointer = "polkadotapp://stack?key=0x…&url=…", + descriptor = webView.context.assets.open("trinity-stack.json").use { it.readBytes() }, + allowsReservedValues = false, + ), // Optional: activate a local signing session from host-held BIP-39 entropy // (no SSO pairing). Omit for the QR pairing flow. localSessionSecret = null, diff --git a/android/truapi-host/src/main/kotlin/io/parity/truapi/TrUAPIHost.kt b/android/truapi-host/src/main/kotlin/io/parity/truapi/TrUAPIHost.kt index effc8ef983..fd3089f5bb 100644 --- a/android/truapi-host/src/main/kotlin/io/parity/truapi/TrUAPIHost.kt +++ b/android/truapi-host/src/main/kotlin/io/parity/truapi/TrUAPIHost.kt @@ -51,7 +51,6 @@ import uniffi.truapi.HostThemeSubscribeItem import uniffi.truapi.ThemeName import uniffi.truapi.ThemeVariant import uniffi.truapi.AuthState -import uniffi.truapi.HostChainSet import uniffi.truapi.PermissionAuthorizationRequest import uniffi.truapi.PermissionAuthorizationStatus import uniffi.truapi.PermissionDecision @@ -317,13 +316,6 @@ interface HostBridge { @Throws(HostRejection::class) suspend fun featureSupported(request: HostFeatureSupportedRequest): Boolean - /** - * Enumerate the chains this host serves: its environment plus one entry - * per chain role. - */ - @Throws(HostRejection::class) - fun supportedChains(): HostChainSet = HostChainSet(network = "", chains = emptyList()) - /** * The chains this host builds as V5 general transactions, each with its * transaction extension pipeline; every other chain is built as V4. The @@ -639,9 +631,6 @@ private class HostCallbackAdapter(private val bridge: HostBridge) : HostCallback override suspend fun featureSupported(request: HostFeatureSupportedRequest): Boolean = withHostRejection { bridge.featureSupported(request) } - override fun supportedChains(): HostChainSet = - withHostRejection { bridge.supportedChains() } - override suspend fun transactionExtensionVersions(): ExtensionVersionMap = withHostRejection { bridge.transactionExtensionVersions() } diff --git a/docs/rfcs/0022-account-derivations.md b/docs/rfcs/0022-account-derivations.md index ecdeab3c14..f997d9770c 100644 --- a/docs/rfcs/0022-account-derivations.md +++ b/docs/rfcs/0022-account-derivations.md @@ -281,7 +281,7 @@ the network the host runs against: `uid.dot` and `peopl.dot` on Polkadot, runtime scopes its product contexts with (`product/peopl./…`), so accounts, contexts and keys agree on which network a person belongs to, and one seed is one person per network. A host learns the suffix from the network it is configured -for (`SigningHostConfig::network_suffix` in the Rust core) and never assumes it. +for (the `tld` of its stack descriptor, `Stack::tld` in the Rust core) and never assumes it. ### Well-known alias accounts The runtime defines well-known Account Contexts (`resources`, `score`, diff --git a/hosts/android/.claude/docs/architecture/host-api-products.md b/hosts/android/.claude/docs/architecture/host-api-products.md index 5b612bc406..bbd7d88467 100644 --- a/hosts/android/.claude/docs/architecture/host-api-products.md +++ b/hosts/android/.claude/docs/architecture/host-api-products.md @@ -184,8 +184,8 @@ The core is one process-wide `TrUAPIHostRuntime` with a `TrUAPIProductExecution` per product connection, and the Android side mirrors that split: - `TrUAPIHostRuntimeProvider` (`@Singleton`) builds the runtime lazily on first - use from `HostRuntimeConfig` (host identity, People/Bulletin genesis hashes, - the local session) and keeps it for the process. Its runtime-level + use from `HostRuntimeConfig` (host identity, the Trinity stack the build + bundles, the local session) and keeps it for the process. Its runtime-level `HostBridge` serves only what the signing runtime asks for — core storage, auth state, signing-side chain access, and the confirmations SSO raises. Product-scoped calls have no product there and fail closed, as on iOS. It @@ -235,15 +235,26 @@ it cannot describe. Every variant is covered by - **Dispatcher-thread callbacks** (chain, theme, storage, core log) run inline and **must return promptly** — no blocking work. Chain I/O goes through the non-blocking `WebSocketChainProvider`, one per bridge so each notifies the runtime or execution it dials for; theme is served from a cached value. - `devicePermissionStatus` and `currentLocale` are left on the shell defaults (not applicable / system locale) — wire them when a product needs them. +### Stack + +The core takes every chain it uses, and the chain info it reports to products, +from the Trinity stack descriptor in `HostRuntimeConfig.stack`. `TrUAPIStack` +holds each stack's pointer and maps the build's `TestnetEnvironment` to one: +`TESTNET` runs previewnet, `NIGHTLY` paseo-next-v2 and `PRODUCTION` Polkadot, +the only one allowed Polkadot's reserved genesis hashes and `dot` TLD. The +signed descriptors ship byte for byte under +`feature/products/impl/src/main/assets/stacks/`, and each verifies only under +the key in its own pointer, so refresh a copy and its pointer together. A copy +the core refuses fails the runtime boot. + ### Chain agreement -`supportedChains`, `featureSupported(Chain)` and `chainConnect` all answer from -one `TrUAPIChains` snapshot — resolved at `attach` per execution, and once at -boot for the runtime — through -`TrUAPIChains.canDial`. Never answer any of them from the registry directly: the -registry holds chains with no `wss` endpoints, so the host would advertise or -promise a chain `chainConnect` then refuses. `TrUAPIChainAgreementTest` pins the -invariant. +`featureSupported(Chain)` and `chainConnect` both answer from one +`TrUAPIChains` snapshot — resolved at `attach` per execution, and once at boot +for the runtime — through `TrUAPIChains.canDial`. Never answer either from the +registry directly: the registry holds chains with no `wss` endpoints, so the +host would promise a chain `chainConnect` then refuses. +`TrUAPIChainAgreementTest` pins the invariant. ### Storage diff --git a/hosts/android/app/src/androidTest/java/io/paritytech/polkadotapp/app/MediaPermissionIntegrationTest.kt b/hosts/android/app/src/androidTest/java/io/paritytech/polkadotapp/app/MediaPermissionIntegrationTest.kt index 4232c1d97f..f33cea5d47 100644 --- a/hosts/android/app/src/androidTest/java/io/paritytech/polkadotapp/app/MediaPermissionIntegrationTest.kt +++ b/hosts/android/app/src/androidTest/java/io/paritytech/polkadotapp/app/MediaPermissionIntegrationTest.kt @@ -19,6 +19,7 @@ import io.paritytech.polkadotapp.common.utils.permissions.PermissionResult import io.paritytech.polkadotapp.feature_products_impl.domain.hostApi.FixedProductId import io.paritytech.polkadotapp.feature_products_impl.domain.permissions.handlers.DeviceCapabilityPermissionHandler import io.paritytech.polkadotapp.feature_products_impl.domain.truapi.TrUAPIBootstrapInstaller +import io.paritytech.polkadotapp.feature_products_impl.domain.truapi.TrUAPIStack import io.paritytech.polkadotapp.feature_products_impl.domain.webView.ProductWebChromeClient import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -66,10 +67,7 @@ class MediaPermissionIntegrationTest : BaseIntegrationTest() { } val config = HostRuntimeConfig( hostName = "Media permission test", - peopleChainGenesisHash = ByteArray(32), - bulletinChainGenesisHash = ByteArray(32), - assetHubChainGenesisHash = ByteArray(32), - networkSuffix = "paseo", + stack = TrUAPIStack.PREVIEWNET.bundled(context), databaseDirectory = createTempDirectory("truapi").toString(), ) TrUAPIHostRuntime(bridge, config).use { runtime -> diff --git a/hosts/android/app/src/androidTest/java/io/paritytech/polkadotapp/app/RendererLossRecoveryTest.kt b/hosts/android/app/src/androidTest/java/io/paritytech/polkadotapp/app/RendererLossRecoveryTest.kt index 7bb159e0d3..7ffe9efa4a 100644 --- a/hosts/android/app/src/androidTest/java/io/paritytech/polkadotapp/app/RendererLossRecoveryTest.kt +++ b/hosts/android/app/src/androidTest/java/io/paritytech/polkadotapp/app/RendererLossRecoveryTest.kt @@ -20,6 +20,7 @@ import io.parity.truapi.TrUAPIHostRuntime import io.paritytech.polkadotapp.common.utils.RealCoroutineDispatchers import io.paritytech.polkadotapp.feature_products_impl.domain.hostApi.FixedProductId import io.paritytech.polkadotapp.feature_products_impl.domain.truapi.TrUAPIBootstrapInstaller +import io.paritytech.polkadotapp.feature_products_impl.domain.truapi.TrUAPIStack import io.paritytech.polkadotapp.feature_products_impl.domain.webView.WebViewProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -127,10 +128,7 @@ class RendererLossRecoveryTest : BaseIntegrationTest() { private const val PAGE_URL = "$ORIGIN/" private val CONFIG = HostRuntimeConfig( hostName = "Renderer loss test", - peopleChainGenesisHash = ByteArray(32), - bulletinChainGenesisHash = ByteArray(32), - assetHubChainGenesisHash = ByteArray(32), - networkSuffix = "paseo", + stack = TrUAPIStack.PREVIEWNET.bundled(InstrumentationRegistry.getInstrumentation().targetContext), databaseDirectory = createTempDirectory("truapi").toString(), ) diff --git a/hosts/android/bindings/truapi-host/build.gradle.kts b/hosts/android/bindings/truapi-host/build.gradle.kts index 77297b4e09..51c8e08ac5 100644 --- a/hosts/android/bindings/truapi-host/build.gradle.kts +++ b/hosts/android/bindings/truapi-host/build.gradle.kts @@ -72,7 +72,11 @@ android { ) } getByName("test") { java.srcDirs("src/test/kotlin") } - getByName("androidTest") { java.srcDirs("src/androidTest/kotlin") } + getByName("androidTest") { + java.srcDirs("src/androidTest/kotlin") + // The core's own signed stack copies, so a test runtime starts on a stack the core verifies. + assets.srcDir("$truapiDir/rust/crates/truapi-host-cli/stacks") + } } lint { diff --git a/hosts/android/bindings/truapi-host/src/androidTest/kotlin/io/parity/truapi/ContainerHttpAuthorizationTest.kt b/hosts/android/bindings/truapi-host/src/androidTest/kotlin/io/parity/truapi/ContainerHttpAuthorizationTest.kt index 3153dba5b3..290a399f78 100644 --- a/hosts/android/bindings/truapi-host/src/androidTest/kotlin/io/parity/truapi/ContainerHttpAuthorizationTest.kt +++ b/hosts/android/bindings/truapi-host/src/androidTest/kotlin/io/parity/truapi/ContainerHttpAuthorizationTest.kt @@ -62,10 +62,7 @@ class ContainerHttpAuthorizationTest { val authorizedLoads = AtomicInteger() val config = HostRuntimeConfig( hostName = "HTTP authorization test", - peopleChainGenesisHash = ByteArray(32), - bulletinChainGenesisHash = ByteArray(32), - assetHubChainGenesisHash = ByteArray(32), - networkSuffix = "paseo", + stack = previewnetStack(), databaseDirectory = createTempDirectory("truapi").toString(), ) diff --git a/hosts/android/bindings/truapi-host/src/androidTest/kotlin/io/parity/truapi/InertHostBridge.kt b/hosts/android/bindings/truapi-host/src/androidTest/kotlin/io/parity/truapi/InertHostBridge.kt index 8814d43642..a6cc551712 100644 --- a/hosts/android/bindings/truapi-host/src/androidTest/kotlin/io/parity/truapi/InertHostBridge.kt +++ b/hosts/android/bindings/truapi-host/src/androidTest/kotlin/io/parity/truapi/InertHostBridge.kt @@ -1,6 +1,8 @@ package io.parity.truapi import android.content.Context +import androidx.test.platform.app.InstrumentationRegistry +import uniffi.truapi.BundledStack import uniffi.truapi.HostDevicePermissionRequest import uniffi.truapi.HostFeatureSupportedRequest import uniffi.truapi.HostRuntimeConfig @@ -40,9 +42,17 @@ class InertHostBridge( /** A runtime config whose core database lives in [databaseDirectory]. */ fun instrumentedRuntimeConfig(databaseDirectory: String) = HostRuntimeConfig( hostName = "Instrumented test host", - peopleChainGenesisHash = ByteArray(32) { 0xa2.toByte() }, - bulletinChainGenesisHash = ByteArray(32) { 0xbb.toByte() }, - assetHubChainGenesisHash = ByteArray(32) { 0xcc.toByte() }, - networkSuffix = "paseo", + stack = previewnetStack(), databaseDirectory = databaseDirectory, ) + +/** Previewnet's signed descriptor and link, packaged into the test APK from the core's copies. */ +fun previewnetStack(): BundledStack { + val assets = InstrumentationRegistry.getInstrumentation().context.assets + fun asset(name: String) = assets.open(name).use { it.readBytes() } + return BundledStack( + pointer = asset("previewnet.link").decodeToString().trim(), + descriptor = asset("previewnet.json"), + allowsReservedValues = false, + ) +} diff --git a/hosts/android/bindings/truapi-host/src/androidTest/kotlin/io/parity/truapi/TrUAPIDiagnosticsTest.kt b/hosts/android/bindings/truapi-host/src/androidTest/kotlin/io/parity/truapi/TrUAPIDiagnosticsTest.kt index 836dd43bd0..7d7e2b2833 100644 --- a/hosts/android/bindings/truapi-host/src/androidTest/kotlin/io/parity/truapi/TrUAPIDiagnosticsTest.kt +++ b/hosts/android/bindings/truapi-host/src/androidTest/kotlin/io/parity/truapi/TrUAPIDiagnosticsTest.kt @@ -65,9 +65,9 @@ class TrUAPIDiagnosticsTest { val context = instrumentation.targetContext - // The People-chain statement store backs the SSO/identity path; the - // all-zero genesis is the local-session sentinel. Route any genesis to - // the configured node so chain-touching methods can reach a backend. + // The People-chain statement store backs the SSO/identity path. Route + // any genesis to the configured node so chain-touching methods can + // reach a backend. val chainNode = args.getString("truapi.chainWs") val chainProvider = WebSocketChainProvider({ listOfNotNull(chainNode) }, OkHttpClient()) @@ -107,10 +107,7 @@ class TrUAPIDiagnosticsTest { val config = HostRuntimeConfig( hostName = "Polkadot Android (diagnostics)", hostIcon = "https://dot.li/dotli.png", - peopleChainGenesisHash = ByteArray(32), - bulletinChainGenesisHash = ByteArray(32), - assetHubChainGenesisHash = ByteArray(32), - networkSuffix = "paseo", + stack = previewnetStack(), databaseDirectory = createTempDirectory("truapi").toString(), // 32 bytes of BIP-39 entropy → a deterministic local signing session // (no SSO pairing, fully offline). diff --git a/hosts/android/docs/DEPLOYMENT.md b/hosts/android/docs/DEPLOYMENT.md index faa83d3547..a7ea177ed9 100644 --- a/hosts/android/docs/DEPLOYMENT.md +++ b/hosts/android/docs/DEPLOYMENT.md @@ -372,7 +372,7 @@ instead. Change them there if your fork needs a different cut. | Field | `debug` | `nightly` | `safetynet` | `release` | Effect | |-------|---------|-----------|-------------|-----------|--------| -| `TESTNET_ENVIRONMENT` | `TESTNET` | `NIGHTLY` | `NIGHTLY` | `PRODUCTION` | Chain environment. Only `PRODUCTION` reads the `chains` Remote Config key; the others read `chains_v2` (§4.1). Also enables the faucet top-up outside `PRODUCTION`. | +| `TESTNET_ENVIRONMENT` | `TESTNET` | `NIGHTLY` | `NIGHTLY` | `PRODUCTION` | Chain environment. Only `PRODUCTION` reads the `chains` Remote Config key; the others read `chains_v2` (§4.1). Also picks the Trinity stack the TrUAPI core runs (`TESTNET` previewnet, `NIGHTLY` paseo-next-v2, `PRODUCTION` Polkadot) and enables the faucet top-up outside `PRODUCTION`. | | `SAFETY_MODE` | off | off | on | on | On hides the full feature set: arbitrary `.dot` products, browse tab, full tab bar, all chat extensions, linked devices, product settings, personhood, collectibles. | | `TAB_BAR_CONNECTIVITY_INDICATOR` | on | on | on | on | Chain-health indicators in the tab bar. The docklet has four item slots, so the browse tab shows only when this is off; it is the switch that brings the browse tab back. | | `COINAGE_DEBUG_FEATURES` | on | on | on | off | "Debug features" card under the balance card (breakdown, faucet top-up, log sharing). | diff --git a/hosts/android/feature/products/impl/src/main/assets/stacks/paseo-next-v2.json b/hosts/android/feature/products/impl/src/main/assets/stacks/paseo-next-v2.json new file mode 100644 index 0000000000..fe9e406b19 --- /dev/null +++ b/hosts/android/feature/products/impl/src/main/assets/stacks/paseo-next-v2.json @@ -0,0 +1 @@ +{"$v":1,"payload":"ewogICIkdiI6IDEsCiAgImFzc2V0cyI6IFsKICAgIHsKICAgICAgImNoYWluIjogIkFzc2V0SHViIiwKICAgICAgImlkIjogNTAwMDA0MTMsCiAgICAgICJwYWxsZXQiOiAiQXNzZXRzIiwKICAgICAgInJvbGUiOiAicGF5bWVudCIKICAgIH0KICBdLAogICJjaGFpbnMiOiBbCiAgICB7CiAgICAgICJmdWxsU3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4MDVjMjk0OWFiNGIwNDAxNDA0MTY5NzlmOTM2MjBkZWU0ZWJjNjdiMzBhODMwZTkyZDYyZTFjZDc2YTZhOGMyZCIsCiAgICAgICAgInVybCI6ICJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vcGFzZW8tbmV0d29yay9wYXNlby1jaGFpbi1zcGVjcy8zMWE1ZTJjMmRkYTU2OWRhZGRkZWY4MmFjODJjODRmMTRhZjVhYmY1L3Bhc2VvLnJhdy5qc29uIgogICAgICB9LAogICAgICAiZ2VuZXNpc0hhc2giOiAiMHgzNzQwNTdiZTY3YjM1NTE1MWYyNzFmZjcwYzNkYjk4MzA4YzYyYzhhZGM0OGRjNjcyNGI2YTAwOWExYTAxNGZkIiwKICAgICAgImxpZ2h0U3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4ZjNkZjM4MDY5MTI5NzY3ZDUzMTBmNGIxOWY0NDM0OWNhMTI3YTM1OTQwNjU2OWJiYTRlYTVjMzVhNDg5YTgxZCIsCiAgICAgICAgInVybCI6ICJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vcGFzZW8tbmV0d29yay9wYXNlby1jaGFpbi1zcGVjcy83YjdhZWFkNTc4MWJlMWYxNWM5YmIzZDlkNjVjZDRhNmNmZDUzYjc2L3Bhc2VvLnJhdy5zbW9sLmpzb24iCiAgICAgIH0sCiAgICAgICJyb2xlIjogIlJlbGF5IiwKICAgICAgInJwYyI6IFsKICAgICAgICAid3NzOi8vcGFzZW8tcnBjLm4uZHdlbGxpci5jb20iLAogICAgICAgICJ3c3M6Ly9wYXNlby12Mi5ycGMudHVyYm9mbGFrZXMuaW8iLAogICAgICAgICJ3c3M6Ly9ycGMtcGFzZW8uc3Rha2V3b3JsZC5pbyIsCiAgICAgICAgIndzczovL3JwYy5pbnRlcndlYi1pdC5jb20vcGFzZW8iCiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJmdWxsU3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4YzJkYmJlM2RhZGRmMzAyZTJjMTI2YThmZmJlMjY4OTdmNGM4ZDcwOTgyNjk3MDU1ZjlkM2M1NzYyNzQ0NjVmYiIsCiAgICAgICAgInVybCI6ICJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vcGFyaXR5dGVjaC9jaGFpbnNwZWNzL2VkMGMwNjhhY2M4NWQ3MzE0MGQxYWE5MzcxNjlhNDg0MDQ3ZmMyNWYvcGFzZW8vcGFyYWNoYWluL2Fzc2V0LWh1Yi1uZXh0L3Jhdy5qc29uIgogICAgICB9LAogICAgICAiZ2VuZXNpc0hhc2giOiAiMHg0MzQ5YjAwZTU0ODk3ZTIxMTk2ZmQzMzEwMTVmYzViZTBmMTRlMTE4YmViMDM3NWVkMmJiMTc5MzczN2JiNTdhIiwKICAgICAgImxpZ2h0U3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4MzY3NTFjZmU1NWU3MmUxZjlmNWM5Mzc2ZmFhMTY3YTgxODdiMjczN2E4MWE3Y2ExMDhhMjc5MTgyZGJjMzFjZCIsCiAgICAgICAgInVybCI6ICJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vcGFyaXR5dGVjaC9jaGFpbnNwZWNzLzBkNjgzN2FiYTg2OTRmMjQ0N2M0ZDM1YzEyNDg0YWFjNzZmM2Q0N2IvcGFzZW8vcGFyYWNoYWluL2Fzc2V0LWh1Yi1uZXh0L2xpZ2h0Lmpzb24iCiAgICAgIH0sCiAgICAgICJwYXJhSWQiOiAxNTAwLAogICAgICAicm9sZSI6ICJBc3NldEh1YiIsCiAgICAgICJycGMiOiBbCiAgICAgICAgIndzczovL3Bhc2VvLWFzc2V0LWh1Yi1uZXh0LXJwYy5wb2xrYWRvdC5pbyIKICAgICAgXQogICAgfSwKICAgIHsKICAgICAgImZ1bGxTcGVjIjogewogICAgICAgICJzaGEyNTYiOiAiMHgxMTgxNDZkYWQ2Y2UzODNkMTUyMjJmYjcxZWI0MWE2YmZiYTk4YjZmN2M2OWIyM2ExYzJjZTM2YzRkNmJkMzc1IiwKICAgICAgICAidXJsIjogImh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9wYXJpdHl0ZWNoL2NoYWluc3BlY3MvZWQwYzA2OGFjYzg1ZDczMTQwZDFhYTkzNzE2OWE0ODQwNDdmYzI1Zi9wYXNlby9wYXJhY2hhaW4vcGVvcGxlLW5leHQtc3lzdGVtL3Jhdy5qc29uIgogICAgICB9LAogICAgICAiZ2VuZXNpc0hhc2giOiAiMHg0YTJiNWI3MzdkZTFkYTU5ZTIwOWIwMDAwYTg3NmVjMmZhMjAwMzVkYzM0ZmQyOTJhODQ4ZGEzMmQyNTVhZDQ4IiwKICAgICAgImxpZ2h0U3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4MTQ2ZDg2YzVjNGZkZjY3OWE4YTQ4Y2FlNmNmYTAyNTY1NGMzMmU4ZWQyMGFjMzkxOGY2YzdiOGExOGE5MTEzYiIsCiAgICAgICAgInVybCI6ICJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vcGFyaXR5dGVjaC9jaGFpbnNwZWNzLzBkNjgzN2FiYTg2OTRmMjQ0N2M0ZDM1YzEyNDg0YWFjNzZmM2Q0N2IvcGFzZW8vcGFyYWNoYWluL3Blb3BsZS1uZXh0LXN5c3RlbS9saWdodC5qc29uIgogICAgICB9LAogICAgICAicGFyYUlkIjogMTUwMiwKICAgICAgInJvbGUiOiAiUGVvcGxlIiwKICAgICAgInJwYyI6IFsKICAgICAgICAid3NzOi8vcGFzZW8tcGVvcGxlLW5leHQtc3lzdGVtLXJwYy5wb2xrYWRvdC5pbyIKICAgICAgXSwKICAgICAgInN0YXRlbWVudFN0b3JlIjogdHJ1ZQogICAgfSwKICAgIHsKICAgICAgImZ1bGxTcGVjIjogewogICAgICAgICJzaGEyNTYiOiAiMHhjZDBiNDI1ZTY1Y2Y0M2MwM2E3ZTZlZTlhMjllNDBjZDFiYThhMTA2YWE5ZTUyYWRmMTBhNTIxNGYwYzU5YWY3IiwKICAgICAgICAidXJsIjogImh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9wYXJpdHl0ZWNoL2NoYWluc3BlY3MvODlkODk4MmIzNzNiODY5MjBlMDI3OGYwMDYwZmEwNWZkNmU4ZjM0NC9wYXNlby9wYXJhY2hhaW4vYnVsbGV0aW4tbmV4dC9yYXcuanNvbiIKICAgICAgfSwKICAgICAgImdlbmVzaXNIYXNoIjogIjB4OGNmZTY3MTdkYzRiZWNmZGEyZTEzYzQ4OGExZTIwNjFmZjJkZmVlOTZlN2QwMzExNTdmNzJkMzY3MTZjMGEyMiIsCiAgICAgICJsaWdodFNwZWMiOiB7CiAgICAgICAgInNoYTI1NiI6ICIweDAwYWMwMzY2N2M0NGZiMjY2NThjN2M4ZTYxZTlhNWI5ZWYxNDVkNTEzZTY0ZjkyOTA0Mjc5YzBjZDQwNjJjOWQiLAogICAgICAgICJ1cmwiOiAiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3Bhcml0eXRlY2gvY2hhaW5zcGVjcy8wZDY4MzdhYmE4Njk0ZjI0NDdjNGQzNWMxMjQ4NGFhYzc2ZjNkNDdiL3Bhc2VvL3BhcmFjaGFpbi9idWxsZXRpbi1uZXh0L2xpZ2h0Lmpzb24iCiAgICAgIH0sCiAgICAgICJwYXJhSWQiOiAxNTAxLAogICAgICAicm9sZSI6ICJCdWxsZXRpbiIsCiAgICAgICJycGMiOiBbCiAgICAgICAgIndzczovL3Bhc2VvLWJ1bGxldGluLW5leHQtcnBjLnBvbGthZG90LmlvIgogICAgICBdCiAgICB9CiAgXSwKICAiY29pbmFnZSI6IHsKICAgICJjaGFpbiI6ICJQZW9wbGUiLAogICAgImluc3RhbmNlIjogMAogIH0sCiAgImRvdG5zIjogewogICAgImNvbnRlbnRSZXNvbHZlciI6ICIweDdmNzRkN2NkNTBmNWE4MzQyNzBlMmFkMzk1YTAxYjAxODkxYWIzN2QiLAogICAgImNvbnRyb2xsZXIiOiAiMHhiZGFhMDFiZDFiYTY3ZDcwOWYyYjFmZjI4NmRhMGQ4NTQ5NzdlYTMwIiwKICAgICJjb3N0TW9kZWwiOiAiMHg4YmZkMWYwOTU3ZTczNzE2NzMyZTcyNTgwMmYxMzgzMGI1NjgyZGE0IiwKICAgICJjcmVhdGUzRmFjdG9yeSI6ICIweDg1MzNjNzllMDU4YzVhNjQ4OWNhZmVjYTg2ZGM2MDBlMDI5ZDc1ZjUiLAogICAgIm11bHRpY2FsbDMiOiAiMHhiNDQ2ODAwMGFiZDg3ZDNjNTZjYmZiZDE1MzE2MTIyM2Q3YjEwOWU1IiwKICAgICJuYW1lRXNjcm93IjogIjB4NDg4MWFmYjc4ZTdjOTA4Y2FlODE4MTY4YjkyNjIyOWQ5MzM3NjUyMCIsCiAgICAibmFtZVdoaXRlbGlzdCI6ICIweDQyMDE2NmNkNjdjYTAyMzMwOTRlNDkyYTRiYmE2NzA0NWVkN2MzOGMiLAogICAgInBvcENvbnRyb2xsZXIiOiAiMHhjYzkzMjM0ODYwNmNjMWYzMzE4Y2FkZWM1YTVjZDJjYTQ0N2Y4YTRiIiwKICAgICJwb3BHYXRld2F5IjogIjB4YTg4OWNjYTNmYjRiMDdiOThhMTFjYzU0YzEwZjEzZGRhMjBiYzNkYiIsCiAgICAicG9wTGVucyI6ICIweDMwOWM1ZmYyMWY5MDgyYTUzMjExNTAwYzZmMzNjYTJhMjEwMjRhZTQiLAogICAgInBvcFJlc29sdmVyIjogIjB4ZGFjOTg0ODg0ZWNhOGZjNDQwMTFmMWQ2YzQ5YjI3ODI4MzkwYTcyYiIsCiAgICAicG9wUnVsZXMiOiAiMHg3NDdiNDU2YmUwM2FlYzBiNDJiZDg1YzUxNTEzNzMwZmJkNDVkYTMxIiwKICAgICJwcm90b2NvbFJlZ2lzdHJ5IjogIjB4ZDE5ZTNkMGM5N2NmNTAxMTI1YTA0YTk3NDA1ZTNlNjU5MmZhODQ2ZSIsCiAgICAicmVnaXN0cmFyIjogIjB4NGYwNmU4MThiYTNkOTg3NzA0ZmQ5MWNmM2Q4NjhlNGIwMTkxMDZhYiIsCiAgICAicmVnaXN0cnkiOiAiMHhmMzQwNTRmZDc2YmJmODVmMjE2Y2Y5OTA4MjI2ZDVmMGE3MmU1MGNhIiwKICAgICJyZXNvbHZlciI6ICIweGJkMTE2NWU1NDlkZjk2ZjA4M2MwYTE2ZjYxNTkwOTI3YmMxODcwMDkiLAogICAgInJldmVyc2VSZXNvbHZlciI6ICIweGVlMzg4M2Q3ZWI2MGVlOWJjZDdmM2JjZDhmMmYwNTMwMmE5Y2MwMzUiLAogICAgInN0b3JlRmFjdG9yeSI6ICIweDk5NjA1YTkyNmZjYjQwYWI1MjBmNjU5YzY1MDVlNWZmODYyNzcxZjYiCiAgfSwKICAiZ2VuZXJhdGlvbiI6ICIweDZlYmJjYzgxMTYwZDdkZjI3NTMyYThiNmFkYWE0MmMwZGFjNDMyZWFjNjExYjIwZGZhM2YwMmQ5ZTA4NjhlMWMiLAogICJpZCI6ICJwYXNlby1uZXh0LXYyIiwKICAibmFtZSI6ICJQYXNlbyBOZXh0IHYyIiwKICAibmV0d29yayI6ICJwYXNlbyIsCiAgInNlcXVlbmNlIjogMTc5MzYxNTI4NywKICAic2VydmljZXMiOiBbCiAgICB7CiAgICAgICJraW5kIjogImR1YiIsCiAgICAgICJ1cmxzIjogWwogICAgICAgICJodHRwczovL2lkZW50aXR5LmRvdHNwYXJrLmFwcC8iCiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJraW5kIjogInR1cm4iLAogICAgICAidXJscyI6IFsKICAgICAgICAiaHR0cHM6Ly9pZGVudGl0eS5kb3RzcGFyay5hcHAvIgogICAgICBdCiAgICB9LAogICAgewogICAgICAia2luZCI6ICJzdHVuIiwKICAgICAgInVybHMiOiBbCiAgICAgICAgInN0dW46c3R1bi5jbG91ZGZsYXJlLmNvbTozNDc4IiwKICAgICAgICAic3R1bjpzdHVuLmNsb3VkZmxhcmUuY29tOjQ0MyIKICAgICAgXQogICAgfSwKICAgIHsKICAgICAgImtpbmQiOiAiaXBmcy1nYXRld2F5IiwKICAgICAgInVybHMiOiBbCiAgICAgICAgImh0dHBzOi8vcGFzZW8tYnVsbGV0aW4tbmV4dC1pcGZzLnBvbGthZG90LmlvL2lwZnMvIgogICAgICBdCiAgICB9LAogICAgewogICAgICAia2luZCI6ICJob3AiLAogICAgICAidXJscyI6IFsKICAgICAgICAid3NzOi8vcGFzZW8taG9wLW5leHQtMC5wb2xrYWRvdC5pbyIsCiAgICAgICAgIndzczovL3Bhc2VvLWhvcC1uZXh0LTEucG9sa2Fkb3QuaW8iCiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJjaGFpbklkIjogNDIwNDIwNDE3LAogICAgICAia2luZCI6ICJldGgtcnBjIiwKICAgICAgInVybHMiOiBbCiAgICAgICAgImh0dHBzOi8vZXRoLXJwYy1wYXNlby1uZXh0LnBvbGthZG90LmlvIgogICAgICBdCiAgICB9LAogICAgewogICAgICAia2luZCI6ICJ3ZWItZ2F0ZXdheSIsCiAgICAgICJ1cmxzIjogWwogICAgICAgICJodHRwczovL3Bhc2VvLmxpLyIKICAgICAgXQogICAgfSwKICAgIHsKICAgICAgImtpbmQiOiAiZXZtLWV4cGxvcmVyIiwKICAgICAgInVybHMiOiBbCiAgICAgICAgImh0dHBzOi8vYmxvY2tzY291dC1wYXNlby1uZXh0LnBvbGthZG90LmlvLyIKICAgICAgXQogICAgfQogIF0sCiAgInRsZCI6ICJwYXNlbyIKfQo=","signature":"0x85d877075908dc4059c48561a604e67e8883566ba929b40e1cf93e20a6d99cd5435fb674dd65c1fb30ede47ea156050fe3cce9ba9bc8a3edb775995903600401"} \ No newline at end of file diff --git a/hosts/android/feature/products/impl/src/main/assets/stacks/polkadot.json b/hosts/android/feature/products/impl/src/main/assets/stacks/polkadot.json new file mode 100644 index 0000000000..33ab297446 --- /dev/null +++ b/hosts/android/feature/products/impl/src/main/assets/stacks/polkadot.json @@ -0,0 +1 @@ +{"$v":1,"payload":"ewogICIkdiI6IDEsCiAgImFzc2V0cyI6IFsKICAgIHsKICAgICAgImNoYWluIjogIkFzc2V0SHViIiwKICAgICAgImlkIjogNzg3MywKICAgICAgInBhbGxldCI6ICJBc3NldHMiLAogICAgICAicm9sZSI6ICJwYXltZW50IgogICAgfQogIF0sCiAgImNoYWlucyI6IFsKICAgIHsKICAgICAgImZ1bGxTcGVjIjogewogICAgICAgICJzaGEyNTYiOiAiMHhkOWZlYWUxODZkMGNiNzc2ZDA0MzcyOTQ2ZDdhMTQ1ZWZhMThmZGJhN2IyODM3MzNmYTY1NTc3M2FmNDI2NDU3IiwKICAgICAgICAidXJsIjogImh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9wYXJpdHl0ZWNoL3BvbGthZG90LXNkay85ZDBkYzZlOGZiYjEzMTc0NjYzYWJiNDAwMWUzNjlmMmE2OWYzNTdhL3BvbGthZG90L25vZGUvc2VydmljZS9jaGFpbi1zcGVjcy9wb2xrYWRvdC5qc29uIgogICAgICB9LAogICAgICAiZ2VuZXNpc0hhc2giOiAiMHg5MWIxNzFiYjE1OGUyZDM4NDhmYTIzYTlmMWMyNTE4MmZiOGUyMDMxM2IyYzFlYjQ5MjE5ZGE3YTcwY2U5MGMzIiwKICAgICAgImxpZ2h0U3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4YWIxNWM2N2U5ZDMwNzE0YWI3MWNkN2ZjNWFiNTNiMDZlNGZlODU0YmIxMThkNjJmNjVmN2ExYjliOGQxYTM3NSIsCiAgICAgICAgInVybCI6ICJodHRwczovL3N0YWNrLmFwaS5wY2YubmV0d29yay9jaGFpbnNwZWNzL3BvbGthZG90LmFiMTVjNjdlOWQzMC5zbW9sLmpzb24iCiAgICAgIH0sCiAgICAgICJyb2xlIjogIlJlbGF5IiwKICAgICAgInJwYyI6IFsKICAgICAgICAid3NzOi8vcnBjLnBvbGthZG90LmlvIiwKICAgICAgICAid3NzOi8vcG9sa2Fkb3QtcnBjLm4uZHdlbGxpci5jb20iLAogICAgICAgICJ3c3M6Ly9wb2xrYWRvdC1ycGMucHVibGljbm9kZS5jb20iLAogICAgICAgICJ3c3M6Ly9wb2xrYWRvdC5hcGkub25maW5hbGl0eS5pby9wdWJsaWMtd3MiLAogICAgICAgICJ3c3M6Ly9wb2xrYWRvdC5nYXRvdGVjaC5uZXR3b3JrIiwKICAgICAgICAid3NzOi8vcnBjLXBvbGthZG90LmhlbGl4c3RyZWV0LmlvIiwKICAgICAgICAid3NzOi8vcnBjLXBvbGthZG90Lmx1Y2t5ZnJpZGF5LmlvIiwKICAgICAgICAid3NzOi8vcnBjLXBvbGthZG90LnN0YWtld29ybGQuaW8iLAogICAgICAgICJ3c3M6Ly9ycGMuaW50ZXJ3ZWItaXQuY29tL3BvbGthZG90IgogICAgICBdCiAgICB9LAogICAgewogICAgICAiZnVsbFNwZWMiOiB7CiAgICAgICAgInNoYTI1NiI6ICIweDM4ZDA5OWNjMzM0MzQ5MTE3MThiMTUzM2IwYjVkZjM0NmQyMDU4NjUyMTEwNjMwYjUzNjRkZTRjZjc2NjZlMzYiLAogICAgICAgICJ1cmwiOiAiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3Bhcml0eXRlY2gvcG9sa2Fkb3Qtc2RrLzlkMGRjNmU4ZmJiMTMxNzQ2NjNhYmI0MDAxZTM2OWYyYTY5ZjM1N2EvY3VtdWx1cy9wYXJhY2hhaW5zL2NoYWluLXNwZWNzL2Fzc2V0LWh1Yi1wb2xrYWRvdC5qc29uIgogICAgICB9LAogICAgICAiZ2VuZXNpc0hhc2giOiAiMHg2OGQ1NmYxNWY4NWQzMTM2OTcwZWMxNjk0NjA0MGJjMTc1MjY1NGU5MDYxNDdmN2U0M2U5ZDUzOWQ3YzNkZTJmIiwKICAgICAgImxpZ2h0U3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4ZDY1MTMyNWY3ZGIzYjQxNjRhODMxODU5NjBkMDkyOTYyZGVjMTZhN2JmMjhmN2QwOWFiYjJmYzQyYzNkNjU4ZiIsCiAgICAgICAgInVybCI6ICJodHRwczovL3N0YWNrLmFwaS5wY2YubmV0d29yay9jaGFpbnNwZWNzL2Fzc2V0LWh1Yi1wb2xrYWRvdC5kNjUxMzI1ZjdkYjMuc21vbC5qc29uIgogICAgICB9LAogICAgICAicGFyYUlkIjogMTAwMCwKICAgICAgInJvbGUiOiAiQXNzZXRIdWIiLAogICAgICAicnBjIjogWwogICAgICAgICJ3c3M6Ly9hc3NldC1odWItcnBjLW1lcmN1cnkuYXBpLnBjZi5uZXR3b3JrIiwKICAgICAgICAid3NzOi8vYXNzZXQtaHViLXJwYy12ZW51cy5hcGkucGNmLm5ldHdvcmsiLAogICAgICAgICJ3c3M6Ly9wb2xrYWRvdC1hc3NldC1odWItcnBjLnBvbGthZG90LmlvIiwKICAgICAgICAid3NzOi8vYXNzZXQtaHViLXBvbGthZG90LXJwYy5uLmR3ZWxsaXIuY29tIiwKICAgICAgICAid3NzOi8vYXNzZXQtaHViLXBvbGthZG90LmdhdG90ZWNoLm5ldHdvcmsiLAogICAgICAgICJ3c3M6Ly9ycGMtYXNzZXQtaHViLXBvbGthZG90LmhlbGl4c3RyZWV0LmlvIiwKICAgICAgICAid3NzOi8vcnBjLWFzc2V0LWh1Yi1wb2xrYWRvdC5sdWNreWZyaWRheS5pbyIsCiAgICAgICAgIndzczovL3JwYy1hc3NldC1odWItcG9sa2Fkb3Quc3Rha2V3b3JsZC5pbyIsCiAgICAgICAgIndzczovL3N0YXRlbWludC5hcGkub25maW5hbGl0eS5pby9wdWJsaWMtd3MiCiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJmdWxsU3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4YzkwMzAzYWQyYTQ0OTU0MjQ3NjViNmQ2Yzg3Yjk4YjkyODZjN2IzMjA5NmVjYzcyMWZlMzc2NzdiNDI2N2QwMCIsCiAgICAgICAgInVybCI6ICJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vcGFyaXR5dGVjaC9wb2xrYWRvdC1zZGsvOWQwZGM2ZThmYmIxMzE3NDY2M2FiYjQwMDFlMzY5ZjJhNjlmMzU3YS9jdW11bHVzL3BhcmFjaGFpbnMvY2hhaW4tc3BlY3MvcGVvcGxlLXBvbGthZG90Lmpzb24iCiAgICAgIH0sCiAgICAgICJnZW5lc2lzSGFzaCI6ICIweDY3ZmExNzdhMDk3YmZhMThmNzdlYTk1YWI1NmU5YmNkZmViMGU1YjhhNDBlNDYyOThiYjkzZTE2YjZmYzUwMDgiLAogICAgICAibGlnaHRTcGVjIjogewogICAgICAgICJzaGEyNTYiOiAiMHg3NmVhYzI0NDQ3YmY5MjE5MzY2NTZkMGMwMTE5MjY3OWU4OWEwNGE3YWZkMTVlMjUxZjQ4YmFhZGI3MGYyY2U2IiwKICAgICAgICAidXJsIjogImh0dHBzOi8vc3RhY2suYXBpLnBjZi5uZXR3b3JrL2NoYWluc3BlY3MvcGVvcGxlLXBvbGthZG90Ljc2ZWFjMjQ0NDdiZi5zbW9sLmpzb24iCiAgICAgIH0sCiAgICAgICJwYXJhSWQiOiAxMDA0LAogICAgICAicm9sZSI6ICJQZW9wbGUiLAogICAgICAicnBjIjogWwogICAgICAgICJ3c3M6Ly9wZW9wbGUtcnBjLWVhcnRoLmFwaS5wY2YubmV0d29yayIsCiAgICAgICAgIndzczovL3Blb3BsZS1ycGMtbWFycy5hcGkucGNmLm5ldHdvcmsiLAogICAgICAgICJ3c3M6Ly9wb2xrYWRvdC1wZW9wbGUtcnBjLnBvbGthZG90LmlvIiwKICAgICAgICAid3NzOi8vcGVvcGxlLXBvbGthZG90LXJwYy5uLmR3ZWxsaXIuY29tIiwKICAgICAgICAid3NzOi8vcnBjLXBlb3BsZS1wb2xrYWRvdC5sdWNreWZyaWRheS5pbyIsCiAgICAgICAgIndzczovL3JwYy1wZW9wbGUtcG9sa2Fkb3Quc3Rha2V3b3JsZC5pbyIKICAgICAgXSwKICAgICAgInN0YXRlbWVudFN0b3JlIjogdHJ1ZQogICAgfSwKICAgIHsKICAgICAgImZ1bGxTcGVjIjogewogICAgICAgICJzaGEyNTYiOiAiMHhlNzI1ZGZjNmIwOTZmYTk4ZjI0ZDBhOGQ2YThkMjhiYTE1ZGIyNzQwZjljOWYzZjg1ODk3MTEyY2Q4MjU5YzI1IiwKICAgICAgICAidXJsIjogImh0dHBzOi8vc3RhY2suYXBpLnBjZi5uZXR3b3JrL2NoYWluc3BlY3MvYnVsbGV0aW4tcG9sa2Fkb3QuZTcyNWRmYzZiMDk2LnJhdy5qc29uIgogICAgICB9LAogICAgICAiZ2VuZXNpc0hhc2giOiAiMHgyNzYxYzk1MjU5ZDU5ZTU1YWUzZGFmNzU2YzE0MTNiNDZlNDVhNWEyOTg3Mjk5ZjhlZjhlNWQ4ZTQ3NzZjYmM0IiwKICAgICAgImxpZ2h0U3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4YjMyMzliODI5MjMxYThkZGExMDQ4ZTA2ZDljOGNjMzAyYzQ1NWRiNTVlOTFkNmViYTIxODIzMGM4NjJmNmU1ZSIsCiAgICAgICAgInVybCI6ICJodHRwczovL3N0YWNrLmFwaS5wY2YubmV0d29yay9jaGFpbnNwZWNzL2J1bGxldGluLXBvbGthZG90LmIzMjM5YjgyOTIzMS5zbW9sLmpzb24iCiAgICAgIH0sCiAgICAgICJwYXJhSWQiOiAxMDEwLAogICAgICAicm9sZSI6ICJCdWxsZXRpbiIsCiAgICAgICJycGMiOiBbCiAgICAgICAgIndzczovL2J1bGxldGluLXJwYy1qdXBpdGVyLmFwaS5wY2YubmV0d29yayIsCiAgICAgICAgIndzczovL2J1bGxldGluLXJwYy1zYXR1cm4uYXBpLnBjZi5uZXR3b3JrIiwKICAgICAgICAid3NzOi8vYnVsbGV0aW4tcnBjLnBvbGthZG90LmlvIiwKICAgICAgICAid3NzOi8vcnBjLWJ1bGxldGluLmx1Y2t5ZnJpZGF5LmlvIiwKICAgICAgICAid3NzOi8vcnBjLmludGVyd2ViLWl0LmNvbS9idWxsZXRpbi1wb2xrYWRvdCIKICAgICAgXQogICAgfQogIF0sCiAgImNvaW5hZ2UiOiB7CiAgICAiY2hhaW4iOiAiUGVvcGxlIiwKICAgICJpbnN0YW5jZSI6IDAKICB9LAogICJjb250cmFjdHMiOiB7CiAgICAiYWNjb3VudERhdGFTdG9yZSI6ICIweDE0NmE5MGI4OWVkMjRkY2FhOWM0MDQ4MjY5YTdiNTdhM2I5M2NmODAiCiAgfSwKICAiZG90bnMiOiB7CiAgICAiY29udGVudFJlc29sdmVyIjogIjB4ZjQ2MGM2NDA2MjE3ZTYyYTBkMjcxN2NkNDVkNjM4NWZlNTgzMjAwMSIsCiAgICAiY29udHJvbGxlciI6ICIweDc2YzE5YWJlMTE0MDRlNDM0NDU2NzBmMTMxNzNhZmE2MGJlZDUzZjgiLAogICAgImNvc3RNb2RlbCI6ICIweGM2ZWYwNGRjZDBmN2Q0ZjhlNmQ5MThkODEwMWVjNWY5ZDVhMTYxOTgiLAogICAgImNyZWF0ZTNGYWN0b3J5IjogIjB4Nzk3MThlYThjOTUxNzQ0N2ZjOTYyMjlkY2JkM2YyZmQ5ZWU4ZDJjMiIsCiAgICAibXVsdGljYWxsMyI6ICIweDdiYTE0YzJkZDlhOGQ5NWQ3ZTY2OTRiOTY3NjNmMDg4NmQ5OTMwOWMiLAogICAgIm5hbWVFc2Nyb3ciOiAiMHhkNzYxMzQ1MjJmYWY3ODhmOGRmMTEwZTExNjY0NDFlNDQ2OGQyYzgxIiwKICAgICJuYW1lV2hpdGVsaXN0IjogIjB4ZWMyZjRjMDA3YjUxYzM1ZGVmYTk2YmY2NTRmMzU3ZmM4NTAzNjJhYiIsCiAgICAicG9wQ29udHJvbGxlciI6ICIweGEwNmQxY2IxZTNmZWRmZjdiNWRmYTc3OWI0Y2M2MTcwM2Y4YTQ2YTEiLAogICAgInBvcExlbnMiOiAiMHhlOThmZTI0MzdlZWM1ZTg2N2IyMzI0MDlkMjlkYWU2MWEwMjU1YzMxIiwKICAgICJwb3BSZXNvbHZlciI6ICIweDQxYTU2MDEyNDYxZWZhOGUzZjYyYjQxMDYyNDBhOWMyNzU4ZDk5ZDgiLAogICAgInBvcFJ1bGVzIjogIjB4MDg4YzhkZTY4ZmQzYzE5OWU2NGM2NTE1M2IzYjYyODc2MDQxM2JkNyIsCiAgICAicHJvdG9jb2xSZWdpc3RyeSI6ICIweDM3MTY5M2JlZWFkMjA5ZmJiOTRlMmZlNWE2ZTQzYzNjMTVmZTM3NmYiLAogICAgInJlZ2lzdHJhciI6ICIweDM3NDQyZDA4NmUyODJmN2M0OTdmYzRmZmUwMGQwYTA2Yjg0YTFmODEiLAogICAgInJlZ2lzdHJ5IjogIjB4MWFlOWE3Mjg2NWVjNzA5MmZjNzAzNjg5MmYzMzQ5NzQ4ZDMxYTcxZSIsCiAgICAicmVzb2x2ZXIiOiAiMHg2MjA2MGQ0ZWJiN2MzNzVkMTUxZTIxZmRjYWVkMGI5YTcxNzI3OTc1IiwKICAgICJyZXZlcnNlUmVzb2x2ZXIiOiAiMHgxOTRiYzUwZWQzMWMwOTAzZTU3NjFiMzdiYzg1ZmI3MzcxYTg2ZTc4IiwKICAgICJzdG9yZUZhY3RvcnkiOiAiMHhmZTc1MDM2MTcwYTVjODAwYTkzOWRmNDc2ZDc1NmMzZDhmZjU2YThhIgogIH0sCiAgImdlbmVyYXRpb24iOiAiMHg5MWIxNzFiYjE1OGUyZDM4NDhmYTIzYTlmMWMyNTE4MmZiOGUyMDMxM2IyYzFlYjQ5MjE5ZGE3YTcwY2U5MGMzIiwKICAiaWQiOiAicG9sa2Fkb3QiLAogICJuYW1lIjogIlBvbGthZG90IiwKICAibmV0d29yayI6ICJwb2xrYWRvdCIsCiAgInNlcXVlbmNlIjogMTc5MzUyOTI5OSwKICAic2VydmljZXMiOiBbCiAgICB7CiAgICAgICJraW5kIjogImR1YiIsCiAgICAgICJ1cmxzIjogWwogICAgICAgICJodHRwczovL3BvbGthZG90LWFwcC5hcGkucGNmLm5ldHdvcmsvIgogICAgICBdCiAgICB9LAogICAgewogICAgICAia2luZCI6ICJ0dXJuIiwKICAgICAgInVybHMiOiBbCiAgICAgICAgImh0dHBzOi8vcG9sa2Fkb3QtYXBwLmFwaS5wY2YubmV0d29yay8iCiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJraW5kIjogInN0dW4iLAogICAgICAidXJscyI6IFsKICAgICAgICAic3R1bjp0dXJuLmFwaS5wY2YubmV0d29yazozNDc4IgogICAgICBdCiAgICB9LAogICAgewogICAgICAia2luZCI6ICJpcGZzLWdhdGV3YXkiLAogICAgICAidXJscyI6IFsKICAgICAgICAiaHR0cHM6Ly9pcGZzLmFwaS5wY2YubmV0d29yay9pcGZzLyIKICAgICAgXQogICAgfSwKICAgIHsKICAgICAgImtpbmQiOiAiaG9wIiwKICAgICAgInVybHMiOiBbCiAgICAgICAgIndzczovL2J1bGxldGluLXJwYy1qdXBpdGVyLmFwaS5wY2YubmV0d29yayIKICAgICAgXQogICAgfSwKICAgIHsKICAgICAgImNoYWluSWQiOiA0MjA0MjA0MTksCiAgICAgICJraW5kIjogImV0aC1ycGMiLAogICAgICAidXJscyI6IFsKICAgICAgICAiaHR0cHM6Ly9ldGgtcnBjLnBvbGthZG90LmlvIgogICAgICBdCiAgICB9LAogICAgewogICAgICAia2luZCI6ICJ3ZWItZ2F0ZXdheSIsCiAgICAgICJ1cmxzIjogWwogICAgICAgICJodHRwczovL2RvdC5saS8iCiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJraW5kIjogImV2bS1leHBsb3JlciIsCiAgICAgICJ1cmxzIjogWwogICAgICAgICJodHRwczovL2Jsb2Nrc2NvdXQucG9sa2Fkb3QuaW8vIgogICAgICBdCiAgICB9CiAgXSwKICAidGxkIjogImRvdCIsCiAgInZhbGlkVW50aWwiOiAxODAxMzA1Mjk5Cn0K","signature":"0x65820b1bf166da769b20ce44a80e22051b12ad13b30fa851f25dba26002cf2ada8a945b2f7a9cefda3a148f011dd14b494e0caa6d22ee6dbda327433c4a09c08"} \ No newline at end of file diff --git a/hosts/android/feature/products/impl/src/main/assets/stacks/previewnet.json b/hosts/android/feature/products/impl/src/main/assets/stacks/previewnet.json new file mode 100644 index 0000000000..372a3fc6e5 --- /dev/null +++ b/hosts/android/feature/products/impl/src/main/assets/stacks/previewnet.json @@ -0,0 +1 @@ +{"$v":1,"payload":"ewogICIkdiI6IDEsCiAgImFzc2V0cyI6IFsKICAgIHsKICAgICAgImNoYWluIjogIkFzc2V0SHViIiwKICAgICAgImlkIjogNTAwMDA0MTMsCiAgICAgICJwYWxsZXQiOiAiQXNzZXRzIiwKICAgICAgInJvbGUiOiAicGF5bWVudCIKICAgIH0KICBdLAogICJjaGFpbnMiOiBbCiAgICB7CiAgICAgICJmdWxsU3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4NzkyNjliMGE1OTkxMDkzMGUwM2MyOGEwMmY2OTdmNWFkYzM1ZDMzZjY0NjgwZGNiYzZhZTMwNjc1ZjBjNGQwOSIsCiAgICAgICAgInVybCI6ICJodHRwczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9jaGFpbnNwZWNzL3Bhc2VvLWxvY2FsLmpzb24iCiAgICAgIH0sCiAgICAgICJnZW5lc2lzSGFzaCI6ICIweDg2MDE0NTc1MzY1N2U3M2MyOWI5Mzg4ZmZhMGE4YWViYzY0M2VhODc0MzRiNGIyNzFiNmMzYzNjYzllNmJmOTIiLAogICAgICAibGlnaHRTcGVjIjogewogICAgICAgICJzaGEyNTYiOiAiMHg4NTliNGY0MjdiODYyYjc0OGRjMDRiOWZmOGEwN2ZmNmRlNDAwZTZkYzUyYTc0MzA0YmQyNTdkZmUyYjhmYTEyIiwKICAgICAgICAidXJsIjogImh0dHBzOi8vcHJldmlld25ldC5zdWJzdHJhdGUuZGV2L2NoYWluc3BlY3MvcGFzZW8tbG9jYWwuc21vbC5qc29uIgogICAgICB9LAogICAgICAicm9sZSI6ICJSZWxheSIsCiAgICAgICJycGMiOiBbCiAgICAgICAgIndzczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9yZWxheS9hbGljZSIsCiAgICAgICAgIndzczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9yZWxheS9ib2IiLAogICAgICAgICJ3c3M6Ly9wcmV2aWV3bmV0LnN1YnN0cmF0ZS5kZXYvcmVsYXkvY2hhcmxpZSIsCiAgICAgICAgIndzczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9yZWxheS9kYXZlIiwKICAgICAgICAid3NzOi8vcHJldmlld25ldC5zdWJzdHJhdGUuZGV2L3JlbGF5L2V2ZSIsCiAgICAgICAgIndzczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9yZWxheS9mZXJkaWUiCiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJmdWxsU3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4Yjg1ZTVkYzhjZDU1MWUzNWU1YTVhNGVjNTM1NTc0YjBhMTdlNTg1YmNhMzljYWZkNGJmYzAyZDc4NjAyMjU2ZSIsCiAgICAgICAgInVybCI6ICJodHRwczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9jaGFpbnNwZWNzL2Fzc2V0LWh1Yi1sb2NhbC5qc29uIgogICAgICB9LAogICAgICAiZ2VuZXNpc0hhc2giOiAiMHhiYWM5N2UyM2ZjOGY0YmNjYWU3MmE5OGY4YWViMmJjYWIyMGJmNzU1ODYyMzA0ZTRiNDZhZDY0NzM0NTZlODk2IiwKICAgICAgImxpZ2h0U3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4ODljYWQ3YzE0NDdhZGRhM2QxZTk2MTM1NjA2OWFlMzAwMTM2M2Q3MGU5NmM1MGE3YWY5MGIyYWIwOGJlM2U1MSIsCiAgICAgICAgInVybCI6ICJodHRwczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9jaGFpbnNwZWNzL2Fzc2V0LWh1Yi1sb2NhbC5zbW9sLmpzb24iCiAgICAgIH0sCiAgICAgICJwYXJhSWQiOiAxNTAwLAogICAgICAicm9sZSI6ICJBc3NldEh1YiIsCiAgICAgICJycGMiOiBbCiAgICAgICAgIndzczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9hc3NldC1odWIiCiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJmdWxsU3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4YmU0ZDY2NjRlODhlM2Y1NWNjZDVmNWVmNWU0YTg1ZDhmY2MwZjY5OGFhMDQ0ZDUyZGJhOTZjYzA1N2Q4Y2ViZSIsCiAgICAgICAgInVybCI6ICJodHRwczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9jaGFpbnNwZWNzL2luZGl2aWR1YWxpdHktbG9jYWwuanNvbiIKICAgICAgfSwKICAgICAgImdlbmVzaXNIYXNoIjogIjB4NTVlM2U2ODllY2ZhOWQyZmZmY2Y3ZDMwOWI4MDExOTU2NjcxNDkzOTgyMjMwYmZkMDQyMGM2ODM1NDIyNDllOSIsCiAgICAgICJsaWdodFNwZWMiOiB7CiAgICAgICAgInNoYTI1NiI6ICIweGM1N2UxMzUwYjczNjM1NGQxMDg0Nzg1OTM5MDE3MDQwZjE4OTRhZDE2MzBlYmJmOTc1ZjNkMGQxMGQxOTM3OGEiLAogICAgICAgICJ1cmwiOiAiaHR0cHM6Ly9wcmV2aWV3bmV0LnN1YnN0cmF0ZS5kZXYvY2hhaW5zcGVjcy9pbmRpdmlkdWFsaXR5LWxvY2FsLnNtb2wuanNvbiIKICAgICAgfSwKICAgICAgInBhcmFJZCI6IDE1MDIsCiAgICAgICJyb2xlIjogIlBlb3BsZSIsCiAgICAgICJycGMiOiBbCiAgICAgICAgIndzczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9wZW9wbGUiCiAgICAgIF0sCiAgICAgICJzdGF0ZW1lbnRTdG9yZSI6IHRydWUKICAgIH0sCiAgICB7CiAgICAgICJmdWxsU3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4YWJlNWVlNTM1NmNiODJiZjQ3YmU5OWMzOTQ4NjhmNmMxMTBkMWI1ZmZlYTMzZjg0ZTc1MTA2OGQ4NDhiMTRmMSIsCiAgICAgICAgInVybCI6ICJodHRwczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9jaGFpbnNwZWNzL2J1bGxldGluLWxvY2FsLmpzb24iCiAgICAgIH0sCiAgICAgICJnZW5lc2lzSGFzaCI6ICIweGEwODExOTJiOTBjMWY2YTNmOGU5Y2U3YjJhODI0NmY0MWFmODA1YzY2NDU2Yzg0ZTA1ZmQ5N2MyYjM1MDI0MjUiLAogICAgICAibGlnaHRTcGVjIjogewogICAgICAgICJzaGEyNTYiOiAiMHhiMmE0Y2I1YjkyMDRiNmYxMDI0ZWQzY2I0MWU5ZTM0M2QwZWI2NGJmNzI1YWVhMDg0ZTliZTUyYjVjNjliMTJlIiwKICAgICAgICAidXJsIjogImh0dHBzOi8vcHJldmlld25ldC5zdWJzdHJhdGUuZGV2L2NoYWluc3BlY3MvYnVsbGV0aW4tbG9jYWwuc21vbC5qc29uIgogICAgICB9LAogICAgICAicGFyYUlkIjogMTUwMSwKICAgICAgInJvbGUiOiAiQnVsbGV0aW4iLAogICAgICAicnBjIjogWwogICAgICAgICJ3c3M6Ly9wcmV2aWV3bmV0LnN1YnN0cmF0ZS5kZXYvYnVsbGV0aW4iCiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJmdWxsU3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4Njg2NzAxNWE2OGQzOGEyNTkwNWJkYTM5ODVjZDFiMjUyMzc1ZDNkZDlmZTJlMmZlZmI3ZWVlMTlmZDRjNDI0OCIsCiAgICAgICAgInVybCI6ICJodHRwczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9jaGFpbnNwZWNzL3dlYjMtc3RvcmFnZS1sb2NhbC5qc29uIgogICAgICB9LAogICAgICAiZ2VuZXNpc0hhc2giOiAiMHg2NTdlYThlYmNjZjg3NmQ0MWFmNDQ4NzFkNzU4ODY2NDc5Y2RlMTAwMTM0YjU4ZTlkMDhlZDdlM2U0ZTYwMjgyIiwKICAgICAgImxpZ2h0U3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4YWY5NDk0NGIwY2UzMmIwNDhhYmZjZTY2ZTI2NjhmNzA1NWNmMjhmMmZjNjQzMGQ4NTgyNDE2M2Y3YmFiMTMyYSIsCiAgICAgICAgInVybCI6ICJodHRwczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9jaGFpbnNwZWNzL3dlYjMtc3RvcmFnZS1sb2NhbC5zbW9sLmpzb24iCiAgICAgIH0sCiAgICAgICJwYXJhSWQiOiAxNjAwLAogICAgICAicm9sZSI6ICJXZWIzU3RvcmFnZSIsCiAgICAgICJycGMiOiBbCiAgICAgICAgIndzczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi93ZWIzLXN0b3JhZ2UiCiAgICAgIF0KICAgIH0KICBdLAogICJjb2luYWdlIjogewogICAgImNoYWluIjogIlBlb3BsZSIsCiAgICAiaW5zdGFuY2UiOiAwCiAgfSwKICAiZG90bnMiOiB7CiAgICAiY29udGVudFJlc29sdmVyIjogIjB4N2Y3NGQ3Y2Q1MGY1YTgzNDI3MGUyYWQzOTVhMDFiMDE4OTFhYjM3ZCIsCiAgICAiY29udHJvbGxlciI6ICIweGJkYWEwMWJkMWJhNjdkNzA5ZjJiMWZmMjg2ZGEwZDg1NDk3N2VhMzAiLAogICAgImNvc3RNb2RlbCI6ICIweDhiZmQxZjA5NTdlNzM3MTY3MzJlNzI1ODAyZjEzODMwYjU2ODJkYTQiLAogICAgImNyZWF0ZTNGYWN0b3J5IjogIjB4ODUzM2M3OWUwNThjNWE2NDg5Y2FmZWNhODZkYzYwMGUwMjlkNzVmNSIsCiAgICAibXVsdGljYWxsMyI6ICIweGI0NDY4MDAwYWJkODdkM2M1NmNiZmJkMTUzMTYxMjIzZDdiMTA5ZTUiLAogICAgIm5hbWVFc2Nyb3ciOiAiMHg0ODgxYWZiNzhlN2M5MDhjYWU4MTgxNjhiOTI2MjI5ZDkzMzc2NTIwIiwKICAgICJuYW1lV2hpdGVsaXN0IjogIjB4NDIwMTY2Y2Q2N2NhMDIzMzA5NGU0OTJhNGJiYTY3MDQ1ZWQ3YzM4YyIsCiAgICAicG9wQ29udHJvbGxlciI6ICIweGNjOTMyMzQ4NjA2Y2MxZjMzMThjYWRlYzVhNWNkMmNhNDQ3ZjhhNGIiLAogICAgInBvcExlbnMiOiAiMHhmZTVhNDVmN2ZkNThkMWE2ZmUwOTQ1NWRiNzk5NDA1YjFkY2U5NDExIiwKICAgICJwb3BSZXNvbHZlciI6ICIweGRhYzk4NDg4NGVjYThmYzQ0MDExZjFkNmM0OWIyNzgyODM5MGE3MmIiLAogICAgInBvcFJ1bGVzIjogIjB4NzQ3YjQ1NmJlMDNhZWMwYjQyYmQ4NWM1MTUxMzczMGZiZDQ1ZGEzMSIsCiAgICAicHJvdG9jb2xSZWdpc3RyeSI6ICIweGQxOWUzZDBjOTdjZjUwMTEyNWEwNGE5NzQwNWUzZTY1OTJmYTg0NmUiLAogICAgInJlZ2lzdHJhciI6ICIweDRmMDZlODE4YmEzZDk4NzcwNGZkOTFjZjNkODY4ZTRiMDE5MTA2YWIiLAogICAgInJlZ2lzdHJ5IjogIjB4ZjM0MDU0ZmQ3NmJiZjg1ZjIxNmNmOTkwODIyNmQ1ZjBhNzJlNTBjYSIsCiAgICAicmVzb2x2ZXIiOiAiMHhiZDExNjVlNTQ5ZGY5NmYwODNjMGExNmY2MTU5MDkyN2JjMTg3MDA5IiwKICAgICJyZXZlcnNlUmVzb2x2ZXIiOiAiMHhlZTM4ODNkN2ViNjBlZTliY2Q3ZjNiY2Q4ZjJmMDUzMDJhOWNjMDM1IiwKICAgICJzdG9yZUZhY3RvcnkiOiAiMHg5OTYwNWE5MjZmY2I0MGFiNTIwZjY1OWM2NTA1ZTVmZjg2Mjc3MWY2IgogIH0sCiAgImdlbmVyYXRpb24iOiAiMjAyNi0xMC0yNlQyMjoxOToyNS4zNTZaIiwKICAiaWQiOiAicHJldmlld25ldCIsCiAgIm5hbWUiOiAiUHJldmlld25ldCIsCiAgIm5ldHdvcmsiOiAicGFzZW8iLAogICJzZXF1ZW5jZSI6IDE3OTM3ODIyNDYsCiAgInNlcnZpY2VzIjogWwogICAgewogICAgICAia2luZCI6ICJkdWIiLAogICAgICAidXJscyI6IFsKICAgICAgICAiaHR0cHM6Ly9wcmV2aWV3bmV0LnN1YnN0cmF0ZS5kZXYvZHViLyIKICAgICAgXQogICAgfSwKICAgIHsKICAgICAgImtpbmQiOiAidHVybiIsCiAgICAgICJ1cmxzIjogWwogICAgICAgICJodHRwczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9kdWIvIgogICAgICBdCiAgICB9LAogICAgewogICAgICAia2luZCI6ICJzdHVuIiwKICAgICAgInVybHMiOiBbCiAgICAgICAgInN0dW46cHJldmlld25ldC5zdWJzdHJhdGUuZGV2OjM0NzgiCiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJraW5kIjogImlwZnMtZ2F0ZXdheSIsCiAgICAgICJ1cmxzIjogWwogICAgICAgICJodHRwczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9pcGZzLyIKICAgICAgXQogICAgfSwKICAgIHsKICAgICAgImtpbmQiOiAiaG9wIiwKICAgICAgInVybHMiOiBbCiAgICAgICAgIndzczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9idWxsZXRpbiIKICAgICAgXQogICAgfSwKICAgIHsKICAgICAgImNoYWluSWQiOiA0MjA0MjA0MTcsCiAgICAgICJraW5kIjogImV0aC1ycGMiLAogICAgICAidXJscyI6IFsKICAgICAgICAiaHR0cHM6Ly9wcmV2aWV3bmV0LnN1YnN0cmF0ZS5kZXYvZXRoLXJwYyIKICAgICAgXQogICAgfSwKICAgIHsKICAgICAgImtpbmQiOiAic3RvcmFnZS1wcm92aWRlciIsCiAgICAgICJ1cmxzIjogWwogICAgICAgICJodHRwczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi93ZWIzLXN0b3JhZ2UtcHJvdmlkZXIvIgogICAgICBdCiAgICB9LAogICAgewogICAgICAia2luZCI6ICJ3ZWItZ2F0ZXdheSIsCiAgICAgICJ1cmxzIjogWwogICAgICAgICJodHRwczovL3Rlc3RuZXQubGkvP25ldHdvcms9cHJldmlld25ldCIKICAgICAgXQogICAgfQogIF0sCiAgInRsZCI6ICJ0ZXN0bmV0Igp9Cg==","signature":"0x850dffc9be8cb6f5168a6ff25c0ceef282f08631d46ea645aeb14a07de96f9c6533a5584f608f4a7728b6172a70ea5e5ef764f72b469cd317a67d317ed652d0d"} \ No newline at end of file diff --git a/hosts/android/feature/products/impl/src/main/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/ProductTrUAPIHostBridge.kt b/hosts/android/feature/products/impl/src/main/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/ProductTrUAPIHostBridge.kt index ae8cc14eeb..bb8f5918fd 100644 --- a/hosts/android/feature/products/impl/src/main/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/ProductTrUAPIHostBridge.kt +++ b/hosts/android/feature/products/impl/src/main/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/ProductTrUAPIHostBridge.kt @@ -43,7 +43,6 @@ import timber.log.Timber import uniffi.truapi.AuthState import uniffi.truapi.ExpandedCardFaceOutcome import uniffi.truapi.ExtensionVersionMap -import uniffi.truapi.HostChainSet import uniffi.truapi.HostDevicePermissionRequest import uniffi.truapi.HostFeatureSupportedRequest import uniffi.truapi.HostNavigateToException @@ -240,8 +239,6 @@ class ProductTrUAPIHostBridge @AssistedInject constructor( override fun currentTheme(): HostThemeSubscribeItem = cachedTheme.get() - override fun supportedChains(): HostChainSet = cachedChains.get().advertised - override suspend fun transactionExtensionVersions(): ExtensionVersionMap = extensionVersions.resolve().getOrElse { throw it } diff --git a/hosts/android/feature/products/impl/src/main/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/TrUAPIChainDirectory.kt b/hosts/android/feature/products/impl/src/main/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/TrUAPIChainDirectory.kt index 9d49681c4f..9ea84472b3 100644 --- a/hosts/android/feature/products/impl/src/main/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/TrUAPIChainDirectory.kt +++ b/hosts/android/feature/products/impl/src/main/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/TrUAPIChainDirectory.kt @@ -1,78 +1,34 @@ package io.paritytech.polkadotapp.feature_products_impl.domain.truapi import io.paritytech.polkadotapp.chains.multiNetwork.ChainRegistry -import io.paritytech.polkadotapp.chains.multiNetwork.KnownChains -import io.paritytech.polkadotapp.chains.multiNetwork.chain.model.Chain import io.paritytech.polkadotapp.chains.multiNetwork.chain.model.GenesisHash -import io.paritytech.polkadotapp.common.data.network.TestnetEnvironment import io.paritytech.polkadotapp.feature_products_impl.domain.hostApi.HostApiInteractor import kotlinx.coroutines.flow.first -import uniffi.truapi.ChainIdentifier -import uniffi.truapi.HostChainEntry -import uniffi.truapi.HostChainSet import javax.inject.Inject /** - * What chains the host serves and how to reach them. RFC-0026 requires the two - * to agree, and the core asks for them on a thread where it cannot block, so - * both are resolved once per session. + * How to reach the chains the host carries. The core asks on a thread where it + * cannot block, so the endpoints are resolved once per session. */ class TrUAPIChainDirectory @Inject constructor( private val chainRegistry: ChainRegistry, private val hostApiInteractor: HostApiInteractor, - private val knownChains: KnownChains, - private val environment: TestnetEnvironment, ) { suspend fun resolve(): TrUAPIChains { - // One read backs both halves; two reads let them disagree. val chains = runCatching { chainRegistry.currentChains.first() }.getOrDefault(emptyList()) - // Every registered chain, not just the advertised roles: `chainConnect` - // and `featureSupported` both answer from this map, so a product can - // reach a chain the host carries without it holding a named role. + // Every registered chain: `chainConnect` and `featureSupported` both + // answer from this map, so a product can reach any chain the host carries. val endpoints = chains.associate { chain -> chain.genesisHash.hexKey() to hostApiInteractor.chainNodes(chain.genesisHash).getOrDefault(emptyList()) } - return TrUAPIChains( - advertised = advertised(chains, endpoints), - endpoints = endpoints, - ) - } - - private fun advertised(chains: List, endpoints: Map>): HostChainSet { - val byId = chains.associateBy { it.id } - // The relay is AssetHub's parent rather than a KnownChains slot. - val relayId = byId[knownChains.assetHub]?.parentId - val roles = listOfNotNull( - relayId?.let { ChainIdentifier.RELAY to it }, - ChainIdentifier.ASSET_HUB to knownChains.assetHub, - ChainIdentifier.PEOPLE to knownChains.people, - ChainIdentifier.BULLETIN to knownChains.bulletIn, - ).mapNotNull { (identifier, chainId) -> - byId[chainId]?.let { identifier to it.genesisHash } - } - - return advertisedFrom(roles, endpoints, environment.ecosystem()) + return TrUAPIChains(endpoints = endpoints) } } -/** Never advertise a chain `chainConnect` cannot dial: no endpoints, no entry. */ -internal fun advertisedFrom( - roles: List>, - endpoints: Map>, - network: String, -): HostChainSet = HostChainSet( - network = network, - chains = roles.mapNotNull { (identifier, genesisHash) -> - if (endpoints[genesisHash.hexKey()].isNullOrEmpty()) null - else HostChainEntry(identifier, genesisHash.value) - }, -) - class TrUAPIChains( - val advertised: HostChainSet, /** Genesis hash, lower-case hex, to its JSON-RPC endpoints in preference order. */ val endpoints: Map>, ) { @@ -83,23 +39,10 @@ class TrUAPIChains( fun canDial(genesisHash: ByteArray): Boolean = !endpoints[genesisHash.hexKey()].isNullOrEmpty() } -internal val EMPTY_CHAINS = TrUAPIChains( - advertised = HostChainSet(network = "", chains = emptyList()), - endpoints = emptyMap(), -) +internal val EMPTY_CHAINS = TrUAPIChains(endpoints = emptyMap()) @OptIn(ExperimentalStdlibApi::class) internal fun GenesisHash.hexKey(): String = value.toHexString() @OptIn(ExperimentalStdlibApi::class) internal fun ByteArray.hexKey(): String = toHexString() - -/** - * RFC-0026 `network` is an open ecosystem string, not a mainnet/testnet flag. - * Both app testnets are Paseo-based and the core itself reports "paseo" for - * them; split these apart if a product ever needs to tell nightly from preview. - */ -private fun TestnetEnvironment.ecosystem(): String = when (this) { - TestnetEnvironment.PRODUCTION -> "polkadot" - TestnetEnvironment.NIGHTLY, TestnetEnvironment.TESTNET -> "paseo" -} diff --git a/hosts/android/feature/products/impl/src/main/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/TrUAPIHostRuntimeProvider.kt b/hosts/android/feature/products/impl/src/main/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/TrUAPIHostRuntimeProvider.kt index 02f17a2fc3..3810d77d15 100644 --- a/hosts/android/feature/products/impl/src/main/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/TrUAPIHostRuntimeProvider.kt +++ b/hosts/android/feature/products/impl/src/main/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/TrUAPIHostRuntimeProvider.kt @@ -8,9 +8,8 @@ import io.parity.truapi.HostCoreStorage import io.parity.truapi.HostStorage import io.parity.truapi.TrUAPIHostRuntime import io.parity.truapi.WebSocketChainProvider -import io.paritytech.polkadotapp.chains.multiNetwork.ChainRegistry -import io.paritytech.polkadotapp.chains.multiNetwork.KnownChains import io.paritytech.polkadotapp.common.data.app.AppLifecycleState +import io.paritytech.polkadotapp.common.data.network.TestnetEnvironment import io.paritytech.polkadotapp.common.data.storage.preferences.encrypted.EncryptedPreferences import io.paritytech.polkadotapp.common.presentation.AppLifecycleObserver import io.paritytech.polkadotapp.common.utils.CoroutineDispatchers @@ -43,7 +42,6 @@ import okhttp3.OkHttpClient import timber.log.Timber import uniffi.truapi.AuthState import uniffi.truapi.ExtensionVersionMap -import uniffi.truapi.HostChainSet import uniffi.truapi.HostDevicePermissionRequest import uniffi.truapi.HostFeatureSupportedRequest import uniffi.truapi.HostLocalStorageReadException @@ -67,8 +65,7 @@ import javax.inject.Singleton @Singleton class TrUAPIHostRuntimeProvider @Inject constructor( @param:ApplicationContext private val context: Context, - private val chainRegistry: ChainRegistry, - private val knownChains: KnownChains, + private val environment: TestnetEnvironment, private val chainDirectory: TrUAPIChainDirectory, private val localSessionSource: TrUAPILocalSessionSource, private val accountRepository: AccountRepository, @@ -145,21 +142,17 @@ class TrUAPIHostRuntimeProvider @Inject constructor( } private suspend fun buildRuntimeConfig(): HostRuntimeConfig { - val peopleGenesis = chainRegistry.getChain(knownChains.people).genesisHash.value - val bulletinGenesis = chainRegistry.getChain(knownChains.bulletIn).genesisHash.value - val assetHubGenesis = chainRegistry.getChain(knownChains.assetHub).genesisHash.value // Booting without a session is the pre-session behaviour: products load // and every signing call fails. Worth degrading to rather than refusing // every product outright. val localSession = localSessionSource.resolve() .logFailure("TrUAPI local session unavailable; booting the host runtime without one") .getOrNull() - // The core derives the wallet's reserved identities under this TLD, so it has - // to be the one the app's own built-in accounts derive from. A wrong suffix - // mints key material that belongs to no network, so this takes the one an - // earlier run read from the chain, else reads it, and fails the boot when - // neither answers. A saved suffix is not a guess: a build has one network. - // Failing releases the memoised boot so the next caller tries again. + // The pinned cards are backed by products named under this TLD, so it has to + // be the network's own. This takes the one an earlier run read from the chain, + // else reads it, and fails the boot when neither answers. A saved suffix is not + // a guess: a build has one network. Failing releases the memoised boot so the + // next caller tries again. val tld = dotNsTldProvider.currentTldOrNull() ?: withTimeoutOrNull(TLD_RESOLVE_TIMEOUT_MS) { dotNsTldProvider.getTldRetrying() } ?: error("dotNS TLD unresolved after ${TLD_RESOLVE_TIMEOUT_MS}ms") @@ -167,10 +160,7 @@ class TrUAPIHostRuntimeProvider @Inject constructor( return HostRuntimeConfig( hostName = HOST_NAME, hostVersion = hostVersion(), - peopleChainGenesisHash = peopleGenesis, - bulletinChainGenesisHash = bulletinGenesis, - assetHubChainGenesisHash = assetHubGenesis, - networkSuffix = tld.value, + stack = TrUAPIStack.of(environment).bundled(context), localSessionSecret = localSession?.secret, localSessionLiteUsername = localSession?.liteUsername, databaseDirectory = context.noBackupFilesDir.resolve(DATABASE_DIRECTORY).apply { mkdirs() }.absolutePath, @@ -283,8 +273,6 @@ class TrUAPIHostRuntimeProvider @Inject constructor( is HostFeatureSupportedRequest.Chain -> cachedChains.get().canDial(request.genesisHash) } - override fun supportedChains(): HostChainSet = cachedChains.get().advertised - override suspend fun transactionExtensionVersions(): ExtensionVersionMap = extensionVersions.resolve().getOrElse { throw it } diff --git a/hosts/android/feature/products/impl/src/main/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/TrUAPIStack.kt b/hosts/android/feature/products/impl/src/main/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/TrUAPIStack.kt new file mode 100644 index 0000000000..1d91a26217 --- /dev/null +++ b/hosts/android/feature/products/impl/src/main/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/TrUAPIStack.kt @@ -0,0 +1,50 @@ +package io.paritytech.polkadotapp.feature_products_impl.domain.truapi + +import android.content.Context +import io.paritytech.polkadotapp.common.data.network.TestnetEnvironment +import uniffi.truapi.BundledStack + +/** + * The Trinity stacks the app ships, which the core takes every chain it uses from. Each [asset] is + * the signed descriptor as served and only verifies under the key in its own [pointer], so the two + * are refreshed together. + */ +enum class TrUAPIStack( + private val pointer: String, + private val asset: String, + // The core refuses Polkadot's genesis hashes and `dot` TLD from any stack without this. + private val allowsReservedValues: Boolean, +) { + PREVIEWNET( + pointer = "polkadotapp://stack?key=0x4d43362f03d5511632a210632d1fb7760b5a663580f12ffca34619f79357e239" + + "&url=https%3A%2F%2Fpreviewnet.substrate.dev%2F.well-known%2Ftrinity-stack.json", + asset = "stacks/previewnet.json", + allowsReservedValues = false, + ), + PASEO_NEXT_V2( + pointer = "polkadotapp://stack?key=0x2b89d8ca74aa2e11ab55f5af1168cd6929b88d1998aecd979b42cfc1078d5377" + + "&url=https%3A%2F%2Fstack-paseo-next.parity-testnet.parity.io%2F.well-known%2Ftrinity-stack.json", + asset = "stacks/paseo-next-v2.json", + allowsReservedValues = false, + ), + POLKADOT( + pointer = "polkadotapp://stack?key=0xdcddb708759dade2e3850368ce0add2072f424ba53f1a2128f3339d646cffe28" + + "&url=https%3A%2F%2Fstack.api.pcf.network%2F.well-known%2Ftrinity-stack.json", + asset = "stacks/polkadot.json", + allowsReservedValues = true, + ); + + fun bundled(context: Context): BundledStack = BundledStack( + pointer = pointer, + descriptor = context.assets.open(asset).use { it.readBytes() }, + allowsReservedValues = allowsReservedValues, + ) + + companion object { + fun of(environment: TestnetEnvironment): TrUAPIStack = when (environment) { + TestnetEnvironment.TESTNET -> PREVIEWNET + TestnetEnvironment.NIGHTLY -> PASEO_NEXT_V2 + TestnetEnvironment.PRODUCTION -> POLKADOT + } + } +} diff --git a/hosts/android/feature/products/impl/src/test/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/TrUAPIChainAgreementTest.kt b/hosts/android/feature/products/impl/src/test/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/TrUAPIChainAgreementTest.kt index 0ecce68e75..8353caf5bb 100644 --- a/hosts/android/feature/products/impl/src/test/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/TrUAPIChainAgreementTest.kt +++ b/hosts/android/feature/products/impl/src/test/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/TrUAPIChainAgreementTest.kt @@ -1,64 +1,29 @@ package io.paritytech.polkadotapp.feature_products_impl.domain.truapi import io.paritytech.polkadotapp.common.domain.model.toDataByteArray -import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test -import uniffi.truapi.ChainIdentifier -/** RFC-0026 requires the advertised set and the dialable set to agree. */ +/** `featureSupported(Chain)` must not promise a chain `chainConnect` refuses to dial. */ class TrUAPIChainAgreementTest { private val relay = genesis(1) private val assetHub = genesis(2) @Test - fun `a role with no endpoints is not advertised`() { - val set = advertisedFrom( - roles = listOf(ChainIdentifier.RELAY to relay, ChainIdentifier.ASSET_HUB to assetHub), - endpoints = mapOf(relay.hexKey() to listOf("wss://relay.example")), - network = "polkadot", - ) + fun `a chain with endpoints is dialable`() { + val chains = TrUAPIChains(endpoints = mapOf(relay.hexKey() to listOf("wss://relay.example"))) - assertEquals(listOf(ChainIdentifier.RELAY), set.chains.map { it.identifier }) - } - - @Test - fun `every advertised chain is dialable`() { - val endpoints = mapOf(relay.hexKey() to listOf("wss://relay.example")) - val chains = TrUAPIChains( - advertised = advertisedFrom( - roles = listOf(ChainIdentifier.RELAY to relay, ChainIdentifier.ASSET_HUB to assetHub), - endpoints = endpoints, - network = "polkadot", - ), - endpoints = endpoints, - ) - - assertTrue(chains.advertised.chains.all { chains.canDial(it.genesisHash) }) + assertTrue(chains.canDial(relay.value)) } @Test fun `a chain with no endpoints is not dialable`() { - val chains = TrUAPIChains( - advertised = advertisedFrom(emptyList(), emptyMap(), network = "polkadot"), - endpoints = mapOf(relay.hexKey() to emptyList()), - ) + val chains = TrUAPIChains(endpoints = mapOf(relay.hexKey() to emptyList())) assertFalse(chains.canDial(relay.value)) assertFalse(chains.canDial(assetHub.value)) } - @Test - fun `an empty registry advertises nothing`() { - val set = advertisedFrom( - roles = listOf(ChainIdentifier.RELAY to relay), - endpoints = emptyMap(), - network = "polkadot", - ) - - assertTrue(set.chains.isEmpty()) - } - private fun genesis(seed: Byte) = ByteArray(32) { seed }.toDataByteArray() } diff --git a/hosts/android/feature/products/impl/src/test/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/TrUAPIHostRuntimeProviderTest.kt b/hosts/android/feature/products/impl/src/test/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/TrUAPIHostRuntimeProviderTest.kt index 849a353c7a..204358d904 100644 --- a/hosts/android/feature/products/impl/src/test/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/TrUAPIHostRuntimeProviderTest.kt +++ b/hosts/android/feature/products/impl/src/test/java/io/paritytech/polkadotapp/feature_products_impl/domain/truapi/TrUAPIHostRuntimeProviderTest.kt @@ -8,9 +8,10 @@ import io.mockk.mockk import io.mockk.mockkObject import io.mockk.slot import io.mockk.unmockkObject -import io.paritytech.polkadotapp.chains.multiNetwork.KnownChains +import io.paritytech.polkadotapp.common.data.network.TestnetEnvironment import io.paritytech.polkadotapp.feature_dotns_api.domain.DotNsTld import io.paritytech.polkadotapp.feature_products_impl.domain.pocket.FakePinnedPocketCards +import io.paritytech.polkadotapp.feature_products_impl.domain.pocket.PinnedPocketCards import io.paritytech.polkadotapp.feature_products_impl.domain.pocket.personhoodProduct import io.paritytech.polkadotapp.feature_products_impl.domain.pocket.pinnedCard import io.paritytech.polkadotapp.test_shared.testDispatchers @@ -18,15 +19,21 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder +import uniffi.truapi.BundledStack import uniffi.truapi.HostRuntimeConfig import uniffi.truapi.NativeTrUApiHostRuntime import uniffi.truapi.PinnedPocketCard +import java.io.File +import java.util.Base64 class TrUAPIHostRuntimeProviderTest { @get:Rule @@ -34,6 +41,7 @@ class TrUAPIHostRuntimeProviderTest { private val runtimeConfig = slot() private val paseo = requireNotNull(DotNsTld.parse("paseo")) + private val pinnedHumanity = listOf(PinnedPocketCard(personhoodProduct, "humanity", "humanity")) @Before fun captureRuntimeConfig() { @@ -55,28 +63,57 @@ class TrUAPIHostRuntimeProviderTest { // name shows no cards offline. The name an earlier run read is the one this build's network has. @Test fun `boots offline on the network name an earlier run saved`() = runTest { - provider(saved = paseo, read = Result.failure(IllegalStateException("offline"))).runtime().getOrThrow() + provider( + saved = paseo, + read = Result.failure(IllegalStateException("offline")), + pinnedPocketCards = mockk { every { cards(paseo) } returns pinnedHumanity }, + ).runtime().getOrThrow() - assertEquals("paseo", runtimeConfig.captured.networkSuffix) + assertEquals(pinnedHumanity, runtimeConfig.captured.pinnedPocketCards) } @Test fun `starts the core with the cards the host pins`() = runTest { provider().runtime().getOrThrow() + assertEquals(pinnedHumanity, runtimeConfig.captured.pinnedPocketCards) + } + + // The core derives the wallet's identities under the stack's TLD, so a build on another + // network's stack mints keys for that network. Only the Polkadot build may use its reserved values. + @Test + fun `starts the core on the stack its chain environment bundles`() = runTest { + val started = TestnetEnvironment.entries.associateWith { environment -> + provider(environment = environment).runtime().getOrThrow() + runtimeConfig.captured.stack.let { StartedStack(it.tld(), it.allowsReservedValues) } + } + assertEquals( - listOf(PinnedPocketCard(personhoodProduct, "humanity", "humanity")), - runtimeConfig.captured.pinnedPocketCards, + mapOf( + TestnetEnvironment.TESTNET to StartedStack(tld = "testnet", allowsReservedValues = false), + TestnetEnvironment.NIGHTLY to StartedStack(tld = "paseo", allowsReservedValues = false), + TestnetEnvironment.PRODUCTION to StartedStack(tld = "dot", allowsReservedValues = true), + ), + started, ) } + private data class StartedStack(val tld: String, val allowsReservedValues: Boolean) + + private fun BundledStack.tld(): String { + val envelope = Json.parseToJsonElement(descriptor.decodeToString()).jsonObject + val payload = Base64.getDecoder().decode(envelope.getValue("payload").jsonPrimitive.content) + return Json.parseToJsonElement(payload.decodeToString()).jsonObject.getValue("tld").jsonPrimitive.content + } + private fun TestScope.provider( saved: DotNsTld? = null, read: Result = Result.success(paseo), + environment: TestnetEnvironment = TestnetEnvironment.NIGHTLY, + pinnedPocketCards: PinnedPocketCards = FakePinnedPocketCards(listOf(pinnedCard(personhoodProduct, "humanity"))), ) = TrUAPIHostRuntimeProvider( context = context(), - chainRegistry = mockk(relaxed = true), - knownChains = KnownChains(people = "people", assetHub = "asset-hub", bulletIn = "bullet-in", hydration = null), + environment = environment, chainDirectory = mockk(relaxed = true), localSessionSource = mockk { coEvery { resolve() } returns Result.failure(IllegalStateException()) }, accountRepository = mockk { every { walletAccountFlow() } returns emptyFlow() }, @@ -94,7 +131,7 @@ class TrUAPIHostRuntimeProviderTest { scannerBridge = mockk(), installations = mockk(), extensionVersions = mockk(), - pinnedPocketCards = FakePinnedPocketCards(listOf(pinnedCard(personhoodProduct, "humanity"))), + pinnedPocketCards = pinnedPocketCards, workerSupervisor = { mockk(relaxed = true) }, dispatchers = testDispatchers(), ) @@ -106,6 +143,8 @@ class TrUAPIHostRuntimeProviderTest { every { packageName } returns "io.parity.polkadotapp" every { packageManager.getPackageInfo("io.parity.polkadotapp", 0) } returns packageInfo every { noBackupFilesDir } returns tempFolder.root + // The copies the app ships, so a stack missing from the assets fails here rather than at launch. + every { assets.open(any()) } answers { File("src/main/assets", firstArg()).inputStream() } } } } diff --git a/hosts/ios/polkadot-app/Modules/Products/TrUAPI/RustHostRuntimeBridge.swift b/hosts/ios/polkadot-app/Modules/Products/TrUAPI/RustHostRuntimeBridge.swift index 4affb91513..e4c00fba5b 100644 --- a/hosts/ios/polkadot-app/Modules/Products/TrUAPI/RustHostRuntimeBridge.swift +++ b/hosts/ios/polkadot-app/Modules/Products/TrUAPI/RustHostRuntimeBridge.swift @@ -141,10 +141,6 @@ final class RustHostRuntimeBridge: HostBridge, @unchecked Sendable { try encryptedStorage.delete(key: key) } - func supportedChains() throws -> HostChainSet { - TrUAPISupportedChains.make(chainRegistry: chainRegistry) - } - func transactionExtensionVersions() async throws -> ExtensionVersionMap { try TrUAPIExtensionVersions.make(chainRegistry: chainRegistry) } diff --git a/hosts/ios/polkadot-app/Modules/Products/TrUAPI/RustProductExecutionBridge.swift b/hosts/ios/polkadot-app/Modules/Products/TrUAPI/RustProductExecutionBridge.swift index 5a05454426..8e53c91974 100644 --- a/hosts/ios/polkadot-app/Modules/Products/TrUAPI/RustProductExecutionBridge.swift +++ b/hosts/ios/polkadot-app/Modules/Products/TrUAPI/RustProductExecutionBridge.swift @@ -184,10 +184,6 @@ class RustProductExecutionBridge: HostBridge, @unchecked Sendable { } } - func supportedChains() throws -> HostChainSet { - TrUAPISupportedChains.make(chainRegistry: dependencies.chainRegistry) - } - func transactionExtensionVersions() async throws -> ExtensionVersionMap { try TrUAPIExtensionVersions.make(chainRegistry: dependencies.chainRegistry) } diff --git a/hosts/ios/polkadot-app/Modules/Products/TrUAPI/Stacks/trinity-stack-paseo-next-v2.json b/hosts/ios/polkadot-app/Modules/Products/TrUAPI/Stacks/trinity-stack-paseo-next-v2.json new file mode 100644 index 0000000000..fe9e406b19 --- /dev/null +++ b/hosts/ios/polkadot-app/Modules/Products/TrUAPI/Stacks/trinity-stack-paseo-next-v2.json @@ -0,0 +1 @@ +{"$v":1,"payload":"ewogICIkdiI6IDEsCiAgImFzc2V0cyI6IFsKICAgIHsKICAgICAgImNoYWluIjogIkFzc2V0SHViIiwKICAgICAgImlkIjogNTAwMDA0MTMsCiAgICAgICJwYWxsZXQiOiAiQXNzZXRzIiwKICAgICAgInJvbGUiOiAicGF5bWVudCIKICAgIH0KICBdLAogICJjaGFpbnMiOiBbCiAgICB7CiAgICAgICJmdWxsU3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4MDVjMjk0OWFiNGIwNDAxNDA0MTY5NzlmOTM2MjBkZWU0ZWJjNjdiMzBhODMwZTkyZDYyZTFjZDc2YTZhOGMyZCIsCiAgICAgICAgInVybCI6ICJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vcGFzZW8tbmV0d29yay9wYXNlby1jaGFpbi1zcGVjcy8zMWE1ZTJjMmRkYTU2OWRhZGRkZWY4MmFjODJjODRmMTRhZjVhYmY1L3Bhc2VvLnJhdy5qc29uIgogICAgICB9LAogICAgICAiZ2VuZXNpc0hhc2giOiAiMHgzNzQwNTdiZTY3YjM1NTE1MWYyNzFmZjcwYzNkYjk4MzA4YzYyYzhhZGM0OGRjNjcyNGI2YTAwOWExYTAxNGZkIiwKICAgICAgImxpZ2h0U3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4ZjNkZjM4MDY5MTI5NzY3ZDUzMTBmNGIxOWY0NDM0OWNhMTI3YTM1OTQwNjU2OWJiYTRlYTVjMzVhNDg5YTgxZCIsCiAgICAgICAgInVybCI6ICJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vcGFzZW8tbmV0d29yay9wYXNlby1jaGFpbi1zcGVjcy83YjdhZWFkNTc4MWJlMWYxNWM5YmIzZDlkNjVjZDRhNmNmZDUzYjc2L3Bhc2VvLnJhdy5zbW9sLmpzb24iCiAgICAgIH0sCiAgICAgICJyb2xlIjogIlJlbGF5IiwKICAgICAgInJwYyI6IFsKICAgICAgICAid3NzOi8vcGFzZW8tcnBjLm4uZHdlbGxpci5jb20iLAogICAgICAgICJ3c3M6Ly9wYXNlby12Mi5ycGMudHVyYm9mbGFrZXMuaW8iLAogICAgICAgICJ3c3M6Ly9ycGMtcGFzZW8uc3Rha2V3b3JsZC5pbyIsCiAgICAgICAgIndzczovL3JwYy5pbnRlcndlYi1pdC5jb20vcGFzZW8iCiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJmdWxsU3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4YzJkYmJlM2RhZGRmMzAyZTJjMTI2YThmZmJlMjY4OTdmNGM4ZDcwOTgyNjk3MDU1ZjlkM2M1NzYyNzQ0NjVmYiIsCiAgICAgICAgInVybCI6ICJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vcGFyaXR5dGVjaC9jaGFpbnNwZWNzL2VkMGMwNjhhY2M4NWQ3MzE0MGQxYWE5MzcxNjlhNDg0MDQ3ZmMyNWYvcGFzZW8vcGFyYWNoYWluL2Fzc2V0LWh1Yi1uZXh0L3Jhdy5qc29uIgogICAgICB9LAogICAgICAiZ2VuZXNpc0hhc2giOiAiMHg0MzQ5YjAwZTU0ODk3ZTIxMTk2ZmQzMzEwMTVmYzViZTBmMTRlMTE4YmViMDM3NWVkMmJiMTc5MzczN2JiNTdhIiwKICAgICAgImxpZ2h0U3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4MzY3NTFjZmU1NWU3MmUxZjlmNWM5Mzc2ZmFhMTY3YTgxODdiMjczN2E4MWE3Y2ExMDhhMjc5MTgyZGJjMzFjZCIsCiAgICAgICAgInVybCI6ICJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vcGFyaXR5dGVjaC9jaGFpbnNwZWNzLzBkNjgzN2FiYTg2OTRmMjQ0N2M0ZDM1YzEyNDg0YWFjNzZmM2Q0N2IvcGFzZW8vcGFyYWNoYWluL2Fzc2V0LWh1Yi1uZXh0L2xpZ2h0Lmpzb24iCiAgICAgIH0sCiAgICAgICJwYXJhSWQiOiAxNTAwLAogICAgICAicm9sZSI6ICJBc3NldEh1YiIsCiAgICAgICJycGMiOiBbCiAgICAgICAgIndzczovL3Bhc2VvLWFzc2V0LWh1Yi1uZXh0LXJwYy5wb2xrYWRvdC5pbyIKICAgICAgXQogICAgfSwKICAgIHsKICAgICAgImZ1bGxTcGVjIjogewogICAgICAgICJzaGEyNTYiOiAiMHgxMTgxNDZkYWQ2Y2UzODNkMTUyMjJmYjcxZWI0MWE2YmZiYTk4YjZmN2M2OWIyM2ExYzJjZTM2YzRkNmJkMzc1IiwKICAgICAgICAidXJsIjogImh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9wYXJpdHl0ZWNoL2NoYWluc3BlY3MvZWQwYzA2OGFjYzg1ZDczMTQwZDFhYTkzNzE2OWE0ODQwNDdmYzI1Zi9wYXNlby9wYXJhY2hhaW4vcGVvcGxlLW5leHQtc3lzdGVtL3Jhdy5qc29uIgogICAgICB9LAogICAgICAiZ2VuZXNpc0hhc2giOiAiMHg0YTJiNWI3MzdkZTFkYTU5ZTIwOWIwMDAwYTg3NmVjMmZhMjAwMzVkYzM0ZmQyOTJhODQ4ZGEzMmQyNTVhZDQ4IiwKICAgICAgImxpZ2h0U3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4MTQ2ZDg2YzVjNGZkZjY3OWE4YTQ4Y2FlNmNmYTAyNTY1NGMzMmU4ZWQyMGFjMzkxOGY2YzdiOGExOGE5MTEzYiIsCiAgICAgICAgInVybCI6ICJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vcGFyaXR5dGVjaC9jaGFpbnNwZWNzLzBkNjgzN2FiYTg2OTRmMjQ0N2M0ZDM1YzEyNDg0YWFjNzZmM2Q0N2IvcGFzZW8vcGFyYWNoYWluL3Blb3BsZS1uZXh0LXN5c3RlbS9saWdodC5qc29uIgogICAgICB9LAogICAgICAicGFyYUlkIjogMTUwMiwKICAgICAgInJvbGUiOiAiUGVvcGxlIiwKICAgICAgInJwYyI6IFsKICAgICAgICAid3NzOi8vcGFzZW8tcGVvcGxlLW5leHQtc3lzdGVtLXJwYy5wb2xrYWRvdC5pbyIKICAgICAgXSwKICAgICAgInN0YXRlbWVudFN0b3JlIjogdHJ1ZQogICAgfSwKICAgIHsKICAgICAgImZ1bGxTcGVjIjogewogICAgICAgICJzaGEyNTYiOiAiMHhjZDBiNDI1ZTY1Y2Y0M2MwM2E3ZTZlZTlhMjllNDBjZDFiYThhMTA2YWE5ZTUyYWRmMTBhNTIxNGYwYzU5YWY3IiwKICAgICAgICAidXJsIjogImh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9wYXJpdHl0ZWNoL2NoYWluc3BlY3MvODlkODk4MmIzNzNiODY5MjBlMDI3OGYwMDYwZmEwNWZkNmU4ZjM0NC9wYXNlby9wYXJhY2hhaW4vYnVsbGV0aW4tbmV4dC9yYXcuanNvbiIKICAgICAgfSwKICAgICAgImdlbmVzaXNIYXNoIjogIjB4OGNmZTY3MTdkYzRiZWNmZGEyZTEzYzQ4OGExZTIwNjFmZjJkZmVlOTZlN2QwMzExNTdmNzJkMzY3MTZjMGEyMiIsCiAgICAgICJsaWdodFNwZWMiOiB7CiAgICAgICAgInNoYTI1NiI6ICIweDAwYWMwMzY2N2M0NGZiMjY2NThjN2M4ZTYxZTlhNWI5ZWYxNDVkNTEzZTY0ZjkyOTA0Mjc5YzBjZDQwNjJjOWQiLAogICAgICAgICJ1cmwiOiAiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3Bhcml0eXRlY2gvY2hhaW5zcGVjcy8wZDY4MzdhYmE4Njk0ZjI0NDdjNGQzNWMxMjQ4NGFhYzc2ZjNkNDdiL3Bhc2VvL3BhcmFjaGFpbi9idWxsZXRpbi1uZXh0L2xpZ2h0Lmpzb24iCiAgICAgIH0sCiAgICAgICJwYXJhSWQiOiAxNTAxLAogICAgICAicm9sZSI6ICJCdWxsZXRpbiIsCiAgICAgICJycGMiOiBbCiAgICAgICAgIndzczovL3Bhc2VvLWJ1bGxldGluLW5leHQtcnBjLnBvbGthZG90LmlvIgogICAgICBdCiAgICB9CiAgXSwKICAiY29pbmFnZSI6IHsKICAgICJjaGFpbiI6ICJQZW9wbGUiLAogICAgImluc3RhbmNlIjogMAogIH0sCiAgImRvdG5zIjogewogICAgImNvbnRlbnRSZXNvbHZlciI6ICIweDdmNzRkN2NkNTBmNWE4MzQyNzBlMmFkMzk1YTAxYjAxODkxYWIzN2QiLAogICAgImNvbnRyb2xsZXIiOiAiMHhiZGFhMDFiZDFiYTY3ZDcwOWYyYjFmZjI4NmRhMGQ4NTQ5NzdlYTMwIiwKICAgICJjb3N0TW9kZWwiOiAiMHg4YmZkMWYwOTU3ZTczNzE2NzMyZTcyNTgwMmYxMzgzMGI1NjgyZGE0IiwKICAgICJjcmVhdGUzRmFjdG9yeSI6ICIweDg1MzNjNzllMDU4YzVhNjQ4OWNhZmVjYTg2ZGM2MDBlMDI5ZDc1ZjUiLAogICAgIm11bHRpY2FsbDMiOiAiMHhiNDQ2ODAwMGFiZDg3ZDNjNTZjYmZiZDE1MzE2MTIyM2Q3YjEwOWU1IiwKICAgICJuYW1lRXNjcm93IjogIjB4NDg4MWFmYjc4ZTdjOTA4Y2FlODE4MTY4YjkyNjIyOWQ5MzM3NjUyMCIsCiAgICAibmFtZVdoaXRlbGlzdCI6ICIweDQyMDE2NmNkNjdjYTAyMzMwOTRlNDkyYTRiYmE2NzA0NWVkN2MzOGMiLAogICAgInBvcENvbnRyb2xsZXIiOiAiMHhjYzkzMjM0ODYwNmNjMWYzMzE4Y2FkZWM1YTVjZDJjYTQ0N2Y4YTRiIiwKICAgICJwb3BHYXRld2F5IjogIjB4YTg4OWNjYTNmYjRiMDdiOThhMTFjYzU0YzEwZjEzZGRhMjBiYzNkYiIsCiAgICAicG9wTGVucyI6ICIweDMwOWM1ZmYyMWY5MDgyYTUzMjExNTAwYzZmMzNjYTJhMjEwMjRhZTQiLAogICAgInBvcFJlc29sdmVyIjogIjB4ZGFjOTg0ODg0ZWNhOGZjNDQwMTFmMWQ2YzQ5YjI3ODI4MzkwYTcyYiIsCiAgICAicG9wUnVsZXMiOiAiMHg3NDdiNDU2YmUwM2FlYzBiNDJiZDg1YzUxNTEzNzMwZmJkNDVkYTMxIiwKICAgICJwcm90b2NvbFJlZ2lzdHJ5IjogIjB4ZDE5ZTNkMGM5N2NmNTAxMTI1YTA0YTk3NDA1ZTNlNjU5MmZhODQ2ZSIsCiAgICAicmVnaXN0cmFyIjogIjB4NGYwNmU4MThiYTNkOTg3NzA0ZmQ5MWNmM2Q4NjhlNGIwMTkxMDZhYiIsCiAgICAicmVnaXN0cnkiOiAiMHhmMzQwNTRmZDc2YmJmODVmMjE2Y2Y5OTA4MjI2ZDVmMGE3MmU1MGNhIiwKICAgICJyZXNvbHZlciI6ICIweGJkMTE2NWU1NDlkZjk2ZjA4M2MwYTE2ZjYxNTkwOTI3YmMxODcwMDkiLAogICAgInJldmVyc2VSZXNvbHZlciI6ICIweGVlMzg4M2Q3ZWI2MGVlOWJjZDdmM2JjZDhmMmYwNTMwMmE5Y2MwMzUiLAogICAgInN0b3JlRmFjdG9yeSI6ICIweDk5NjA1YTkyNmZjYjQwYWI1MjBmNjU5YzY1MDVlNWZmODYyNzcxZjYiCiAgfSwKICAiZ2VuZXJhdGlvbiI6ICIweDZlYmJjYzgxMTYwZDdkZjI3NTMyYThiNmFkYWE0MmMwZGFjNDMyZWFjNjExYjIwZGZhM2YwMmQ5ZTA4NjhlMWMiLAogICJpZCI6ICJwYXNlby1uZXh0LXYyIiwKICAibmFtZSI6ICJQYXNlbyBOZXh0IHYyIiwKICAibmV0d29yayI6ICJwYXNlbyIsCiAgInNlcXVlbmNlIjogMTc5MzYxNTI4NywKICAic2VydmljZXMiOiBbCiAgICB7CiAgICAgICJraW5kIjogImR1YiIsCiAgICAgICJ1cmxzIjogWwogICAgICAgICJodHRwczovL2lkZW50aXR5LmRvdHNwYXJrLmFwcC8iCiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJraW5kIjogInR1cm4iLAogICAgICAidXJscyI6IFsKICAgICAgICAiaHR0cHM6Ly9pZGVudGl0eS5kb3RzcGFyay5hcHAvIgogICAgICBdCiAgICB9LAogICAgewogICAgICAia2luZCI6ICJzdHVuIiwKICAgICAgInVybHMiOiBbCiAgICAgICAgInN0dW46c3R1bi5jbG91ZGZsYXJlLmNvbTozNDc4IiwKICAgICAgICAic3R1bjpzdHVuLmNsb3VkZmxhcmUuY29tOjQ0MyIKICAgICAgXQogICAgfSwKICAgIHsKICAgICAgImtpbmQiOiAiaXBmcy1nYXRld2F5IiwKICAgICAgInVybHMiOiBbCiAgICAgICAgImh0dHBzOi8vcGFzZW8tYnVsbGV0aW4tbmV4dC1pcGZzLnBvbGthZG90LmlvL2lwZnMvIgogICAgICBdCiAgICB9LAogICAgewogICAgICAia2luZCI6ICJob3AiLAogICAgICAidXJscyI6IFsKICAgICAgICAid3NzOi8vcGFzZW8taG9wLW5leHQtMC5wb2xrYWRvdC5pbyIsCiAgICAgICAgIndzczovL3Bhc2VvLWhvcC1uZXh0LTEucG9sa2Fkb3QuaW8iCiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJjaGFpbklkIjogNDIwNDIwNDE3LAogICAgICAia2luZCI6ICJldGgtcnBjIiwKICAgICAgInVybHMiOiBbCiAgICAgICAgImh0dHBzOi8vZXRoLXJwYy1wYXNlby1uZXh0LnBvbGthZG90LmlvIgogICAgICBdCiAgICB9LAogICAgewogICAgICAia2luZCI6ICJ3ZWItZ2F0ZXdheSIsCiAgICAgICJ1cmxzIjogWwogICAgICAgICJodHRwczovL3Bhc2VvLmxpLyIKICAgICAgXQogICAgfSwKICAgIHsKICAgICAgImtpbmQiOiAiZXZtLWV4cGxvcmVyIiwKICAgICAgInVybHMiOiBbCiAgICAgICAgImh0dHBzOi8vYmxvY2tzY291dC1wYXNlby1uZXh0LnBvbGthZG90LmlvLyIKICAgICAgXQogICAgfQogIF0sCiAgInRsZCI6ICJwYXNlbyIKfQo=","signature":"0x85d877075908dc4059c48561a604e67e8883566ba929b40e1cf93e20a6d99cd5435fb674dd65c1fb30ede47ea156050fe3cce9ba9bc8a3edb775995903600401"} \ No newline at end of file diff --git a/hosts/ios/polkadot-app/Modules/Products/TrUAPI/Stacks/trinity-stack-polkadot.json b/hosts/ios/polkadot-app/Modules/Products/TrUAPI/Stacks/trinity-stack-polkadot.json new file mode 100644 index 0000000000..33ab297446 --- /dev/null +++ b/hosts/ios/polkadot-app/Modules/Products/TrUAPI/Stacks/trinity-stack-polkadot.json @@ -0,0 +1 @@ +{"$v":1,"payload":"ewogICIkdiI6IDEsCiAgImFzc2V0cyI6IFsKICAgIHsKICAgICAgImNoYWluIjogIkFzc2V0SHViIiwKICAgICAgImlkIjogNzg3MywKICAgICAgInBhbGxldCI6ICJBc3NldHMiLAogICAgICAicm9sZSI6ICJwYXltZW50IgogICAgfQogIF0sCiAgImNoYWlucyI6IFsKICAgIHsKICAgICAgImZ1bGxTcGVjIjogewogICAgICAgICJzaGEyNTYiOiAiMHhkOWZlYWUxODZkMGNiNzc2ZDA0MzcyOTQ2ZDdhMTQ1ZWZhMThmZGJhN2IyODM3MzNmYTY1NTc3M2FmNDI2NDU3IiwKICAgICAgICAidXJsIjogImh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9wYXJpdHl0ZWNoL3BvbGthZG90LXNkay85ZDBkYzZlOGZiYjEzMTc0NjYzYWJiNDAwMWUzNjlmMmE2OWYzNTdhL3BvbGthZG90L25vZGUvc2VydmljZS9jaGFpbi1zcGVjcy9wb2xrYWRvdC5qc29uIgogICAgICB9LAogICAgICAiZ2VuZXNpc0hhc2giOiAiMHg5MWIxNzFiYjE1OGUyZDM4NDhmYTIzYTlmMWMyNTE4MmZiOGUyMDMxM2IyYzFlYjQ5MjE5ZGE3YTcwY2U5MGMzIiwKICAgICAgImxpZ2h0U3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4YWIxNWM2N2U5ZDMwNzE0YWI3MWNkN2ZjNWFiNTNiMDZlNGZlODU0YmIxMThkNjJmNjVmN2ExYjliOGQxYTM3NSIsCiAgICAgICAgInVybCI6ICJodHRwczovL3N0YWNrLmFwaS5wY2YubmV0d29yay9jaGFpbnNwZWNzL3BvbGthZG90LmFiMTVjNjdlOWQzMC5zbW9sLmpzb24iCiAgICAgIH0sCiAgICAgICJyb2xlIjogIlJlbGF5IiwKICAgICAgInJwYyI6IFsKICAgICAgICAid3NzOi8vcnBjLnBvbGthZG90LmlvIiwKICAgICAgICAid3NzOi8vcG9sa2Fkb3QtcnBjLm4uZHdlbGxpci5jb20iLAogICAgICAgICJ3c3M6Ly9wb2xrYWRvdC1ycGMucHVibGljbm9kZS5jb20iLAogICAgICAgICJ3c3M6Ly9wb2xrYWRvdC5hcGkub25maW5hbGl0eS5pby9wdWJsaWMtd3MiLAogICAgICAgICJ3c3M6Ly9wb2xrYWRvdC5nYXRvdGVjaC5uZXR3b3JrIiwKICAgICAgICAid3NzOi8vcnBjLXBvbGthZG90LmhlbGl4c3RyZWV0LmlvIiwKICAgICAgICAid3NzOi8vcnBjLXBvbGthZG90Lmx1Y2t5ZnJpZGF5LmlvIiwKICAgICAgICAid3NzOi8vcnBjLXBvbGthZG90LnN0YWtld29ybGQuaW8iLAogICAgICAgICJ3c3M6Ly9ycGMuaW50ZXJ3ZWItaXQuY29tL3BvbGthZG90IgogICAgICBdCiAgICB9LAogICAgewogICAgICAiZnVsbFNwZWMiOiB7CiAgICAgICAgInNoYTI1NiI6ICIweDM4ZDA5OWNjMzM0MzQ5MTE3MThiMTUzM2IwYjVkZjM0NmQyMDU4NjUyMTEwNjMwYjUzNjRkZTRjZjc2NjZlMzYiLAogICAgICAgICJ1cmwiOiAiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3Bhcml0eXRlY2gvcG9sa2Fkb3Qtc2RrLzlkMGRjNmU4ZmJiMTMxNzQ2NjNhYmI0MDAxZTM2OWYyYTY5ZjM1N2EvY3VtdWx1cy9wYXJhY2hhaW5zL2NoYWluLXNwZWNzL2Fzc2V0LWh1Yi1wb2xrYWRvdC5qc29uIgogICAgICB9LAogICAgICAiZ2VuZXNpc0hhc2giOiAiMHg2OGQ1NmYxNWY4NWQzMTM2OTcwZWMxNjk0NjA0MGJjMTc1MjY1NGU5MDYxNDdmN2U0M2U5ZDUzOWQ3YzNkZTJmIiwKICAgICAgImxpZ2h0U3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4ZDY1MTMyNWY3ZGIzYjQxNjRhODMxODU5NjBkMDkyOTYyZGVjMTZhN2JmMjhmN2QwOWFiYjJmYzQyYzNkNjU4ZiIsCiAgICAgICAgInVybCI6ICJodHRwczovL3N0YWNrLmFwaS5wY2YubmV0d29yay9jaGFpbnNwZWNzL2Fzc2V0LWh1Yi1wb2xrYWRvdC5kNjUxMzI1ZjdkYjMuc21vbC5qc29uIgogICAgICB9LAogICAgICAicGFyYUlkIjogMTAwMCwKICAgICAgInJvbGUiOiAiQXNzZXRIdWIiLAogICAgICAicnBjIjogWwogICAgICAgICJ3c3M6Ly9hc3NldC1odWItcnBjLW1lcmN1cnkuYXBpLnBjZi5uZXR3b3JrIiwKICAgICAgICAid3NzOi8vYXNzZXQtaHViLXJwYy12ZW51cy5hcGkucGNmLm5ldHdvcmsiLAogICAgICAgICJ3c3M6Ly9wb2xrYWRvdC1hc3NldC1odWItcnBjLnBvbGthZG90LmlvIiwKICAgICAgICAid3NzOi8vYXNzZXQtaHViLXBvbGthZG90LXJwYy5uLmR3ZWxsaXIuY29tIiwKICAgICAgICAid3NzOi8vYXNzZXQtaHViLXBvbGthZG90LmdhdG90ZWNoLm5ldHdvcmsiLAogICAgICAgICJ3c3M6Ly9ycGMtYXNzZXQtaHViLXBvbGthZG90LmhlbGl4c3RyZWV0LmlvIiwKICAgICAgICAid3NzOi8vcnBjLWFzc2V0LWh1Yi1wb2xrYWRvdC5sdWNreWZyaWRheS5pbyIsCiAgICAgICAgIndzczovL3JwYy1hc3NldC1odWItcG9sa2Fkb3Quc3Rha2V3b3JsZC5pbyIsCiAgICAgICAgIndzczovL3N0YXRlbWludC5hcGkub25maW5hbGl0eS5pby9wdWJsaWMtd3MiCiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJmdWxsU3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4YzkwMzAzYWQyYTQ0OTU0MjQ3NjViNmQ2Yzg3Yjk4YjkyODZjN2IzMjA5NmVjYzcyMWZlMzc2NzdiNDI2N2QwMCIsCiAgICAgICAgInVybCI6ICJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vcGFyaXR5dGVjaC9wb2xrYWRvdC1zZGsvOWQwZGM2ZThmYmIxMzE3NDY2M2FiYjQwMDFlMzY5ZjJhNjlmMzU3YS9jdW11bHVzL3BhcmFjaGFpbnMvY2hhaW4tc3BlY3MvcGVvcGxlLXBvbGthZG90Lmpzb24iCiAgICAgIH0sCiAgICAgICJnZW5lc2lzSGFzaCI6ICIweDY3ZmExNzdhMDk3YmZhMThmNzdlYTk1YWI1NmU5YmNkZmViMGU1YjhhNDBlNDYyOThiYjkzZTE2YjZmYzUwMDgiLAogICAgICAibGlnaHRTcGVjIjogewogICAgICAgICJzaGEyNTYiOiAiMHg3NmVhYzI0NDQ3YmY5MjE5MzY2NTZkMGMwMTE5MjY3OWU4OWEwNGE3YWZkMTVlMjUxZjQ4YmFhZGI3MGYyY2U2IiwKICAgICAgICAidXJsIjogImh0dHBzOi8vc3RhY2suYXBpLnBjZi5uZXR3b3JrL2NoYWluc3BlY3MvcGVvcGxlLXBvbGthZG90Ljc2ZWFjMjQ0NDdiZi5zbW9sLmpzb24iCiAgICAgIH0sCiAgICAgICJwYXJhSWQiOiAxMDA0LAogICAgICAicm9sZSI6ICJQZW9wbGUiLAogICAgICAicnBjIjogWwogICAgICAgICJ3c3M6Ly9wZW9wbGUtcnBjLWVhcnRoLmFwaS5wY2YubmV0d29yayIsCiAgICAgICAgIndzczovL3Blb3BsZS1ycGMtbWFycy5hcGkucGNmLm5ldHdvcmsiLAogICAgICAgICJ3c3M6Ly9wb2xrYWRvdC1wZW9wbGUtcnBjLnBvbGthZG90LmlvIiwKICAgICAgICAid3NzOi8vcGVvcGxlLXBvbGthZG90LXJwYy5uLmR3ZWxsaXIuY29tIiwKICAgICAgICAid3NzOi8vcnBjLXBlb3BsZS1wb2xrYWRvdC5sdWNreWZyaWRheS5pbyIsCiAgICAgICAgIndzczovL3JwYy1wZW9wbGUtcG9sa2Fkb3Quc3Rha2V3b3JsZC5pbyIKICAgICAgXSwKICAgICAgInN0YXRlbWVudFN0b3JlIjogdHJ1ZQogICAgfSwKICAgIHsKICAgICAgImZ1bGxTcGVjIjogewogICAgICAgICJzaGEyNTYiOiAiMHhlNzI1ZGZjNmIwOTZmYTk4ZjI0ZDBhOGQ2YThkMjhiYTE1ZGIyNzQwZjljOWYzZjg1ODk3MTEyY2Q4MjU5YzI1IiwKICAgICAgICAidXJsIjogImh0dHBzOi8vc3RhY2suYXBpLnBjZi5uZXR3b3JrL2NoYWluc3BlY3MvYnVsbGV0aW4tcG9sa2Fkb3QuZTcyNWRmYzZiMDk2LnJhdy5qc29uIgogICAgICB9LAogICAgICAiZ2VuZXNpc0hhc2giOiAiMHgyNzYxYzk1MjU5ZDU5ZTU1YWUzZGFmNzU2YzE0MTNiNDZlNDVhNWEyOTg3Mjk5ZjhlZjhlNWQ4ZTQ3NzZjYmM0IiwKICAgICAgImxpZ2h0U3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4YjMyMzliODI5MjMxYThkZGExMDQ4ZTA2ZDljOGNjMzAyYzQ1NWRiNTVlOTFkNmViYTIxODIzMGM4NjJmNmU1ZSIsCiAgICAgICAgInVybCI6ICJodHRwczovL3N0YWNrLmFwaS5wY2YubmV0d29yay9jaGFpbnNwZWNzL2J1bGxldGluLXBvbGthZG90LmIzMjM5YjgyOTIzMS5zbW9sLmpzb24iCiAgICAgIH0sCiAgICAgICJwYXJhSWQiOiAxMDEwLAogICAgICAicm9sZSI6ICJCdWxsZXRpbiIsCiAgICAgICJycGMiOiBbCiAgICAgICAgIndzczovL2J1bGxldGluLXJwYy1qdXBpdGVyLmFwaS5wY2YubmV0d29yayIsCiAgICAgICAgIndzczovL2J1bGxldGluLXJwYy1zYXR1cm4uYXBpLnBjZi5uZXR3b3JrIiwKICAgICAgICAid3NzOi8vYnVsbGV0aW4tcnBjLnBvbGthZG90LmlvIiwKICAgICAgICAid3NzOi8vcnBjLWJ1bGxldGluLmx1Y2t5ZnJpZGF5LmlvIiwKICAgICAgICAid3NzOi8vcnBjLmludGVyd2ViLWl0LmNvbS9idWxsZXRpbi1wb2xrYWRvdCIKICAgICAgXQogICAgfQogIF0sCiAgImNvaW5hZ2UiOiB7CiAgICAiY2hhaW4iOiAiUGVvcGxlIiwKICAgICJpbnN0YW5jZSI6IDAKICB9LAogICJjb250cmFjdHMiOiB7CiAgICAiYWNjb3VudERhdGFTdG9yZSI6ICIweDE0NmE5MGI4OWVkMjRkY2FhOWM0MDQ4MjY5YTdiNTdhM2I5M2NmODAiCiAgfSwKICAiZG90bnMiOiB7CiAgICAiY29udGVudFJlc29sdmVyIjogIjB4ZjQ2MGM2NDA2MjE3ZTYyYTBkMjcxN2NkNDVkNjM4NWZlNTgzMjAwMSIsCiAgICAiY29udHJvbGxlciI6ICIweDc2YzE5YWJlMTE0MDRlNDM0NDU2NzBmMTMxNzNhZmE2MGJlZDUzZjgiLAogICAgImNvc3RNb2RlbCI6ICIweGM2ZWYwNGRjZDBmN2Q0ZjhlNmQ5MThkODEwMWVjNWY5ZDVhMTYxOTgiLAogICAgImNyZWF0ZTNGYWN0b3J5IjogIjB4Nzk3MThlYThjOTUxNzQ0N2ZjOTYyMjlkY2JkM2YyZmQ5ZWU4ZDJjMiIsCiAgICAibXVsdGljYWxsMyI6ICIweDdiYTE0YzJkZDlhOGQ5NWQ3ZTY2OTRiOTY3NjNmMDg4NmQ5OTMwOWMiLAogICAgIm5hbWVFc2Nyb3ciOiAiMHhkNzYxMzQ1MjJmYWY3ODhmOGRmMTEwZTExNjY0NDFlNDQ2OGQyYzgxIiwKICAgICJuYW1lV2hpdGVsaXN0IjogIjB4ZWMyZjRjMDA3YjUxYzM1ZGVmYTk2YmY2NTRmMzU3ZmM4NTAzNjJhYiIsCiAgICAicG9wQ29udHJvbGxlciI6ICIweGEwNmQxY2IxZTNmZWRmZjdiNWRmYTc3OWI0Y2M2MTcwM2Y4YTQ2YTEiLAogICAgInBvcExlbnMiOiAiMHhlOThmZTI0MzdlZWM1ZTg2N2IyMzI0MDlkMjlkYWU2MWEwMjU1YzMxIiwKICAgICJwb3BSZXNvbHZlciI6ICIweDQxYTU2MDEyNDYxZWZhOGUzZjYyYjQxMDYyNDBhOWMyNzU4ZDk5ZDgiLAogICAgInBvcFJ1bGVzIjogIjB4MDg4YzhkZTY4ZmQzYzE5OWU2NGM2NTE1M2IzYjYyODc2MDQxM2JkNyIsCiAgICAicHJvdG9jb2xSZWdpc3RyeSI6ICIweDM3MTY5M2JlZWFkMjA5ZmJiOTRlMmZlNWE2ZTQzYzNjMTVmZTM3NmYiLAogICAgInJlZ2lzdHJhciI6ICIweDM3NDQyZDA4NmUyODJmN2M0OTdmYzRmZmUwMGQwYTA2Yjg0YTFmODEiLAogICAgInJlZ2lzdHJ5IjogIjB4MWFlOWE3Mjg2NWVjNzA5MmZjNzAzNjg5MmYzMzQ5NzQ4ZDMxYTcxZSIsCiAgICAicmVzb2x2ZXIiOiAiMHg2MjA2MGQ0ZWJiN2MzNzVkMTUxZTIxZmRjYWVkMGI5YTcxNzI3OTc1IiwKICAgICJyZXZlcnNlUmVzb2x2ZXIiOiAiMHgxOTRiYzUwZWQzMWMwOTAzZTU3NjFiMzdiYzg1ZmI3MzcxYTg2ZTc4IiwKICAgICJzdG9yZUZhY3RvcnkiOiAiMHhmZTc1MDM2MTcwYTVjODAwYTkzOWRmNDc2ZDc1NmMzZDhmZjU2YThhIgogIH0sCiAgImdlbmVyYXRpb24iOiAiMHg5MWIxNzFiYjE1OGUyZDM4NDhmYTIzYTlmMWMyNTE4MmZiOGUyMDMxM2IyYzFlYjQ5MjE5ZGE3YTcwY2U5MGMzIiwKICAiaWQiOiAicG9sa2Fkb3QiLAogICJuYW1lIjogIlBvbGthZG90IiwKICAibmV0d29yayI6ICJwb2xrYWRvdCIsCiAgInNlcXVlbmNlIjogMTc5MzUyOTI5OSwKICAic2VydmljZXMiOiBbCiAgICB7CiAgICAgICJraW5kIjogImR1YiIsCiAgICAgICJ1cmxzIjogWwogICAgICAgICJodHRwczovL3BvbGthZG90LWFwcC5hcGkucGNmLm5ldHdvcmsvIgogICAgICBdCiAgICB9LAogICAgewogICAgICAia2luZCI6ICJ0dXJuIiwKICAgICAgInVybHMiOiBbCiAgICAgICAgImh0dHBzOi8vcG9sa2Fkb3QtYXBwLmFwaS5wY2YubmV0d29yay8iCiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJraW5kIjogInN0dW4iLAogICAgICAidXJscyI6IFsKICAgICAgICAic3R1bjp0dXJuLmFwaS5wY2YubmV0d29yazozNDc4IgogICAgICBdCiAgICB9LAogICAgewogICAgICAia2luZCI6ICJpcGZzLWdhdGV3YXkiLAogICAgICAidXJscyI6IFsKICAgICAgICAiaHR0cHM6Ly9pcGZzLmFwaS5wY2YubmV0d29yay9pcGZzLyIKICAgICAgXQogICAgfSwKICAgIHsKICAgICAgImtpbmQiOiAiaG9wIiwKICAgICAgInVybHMiOiBbCiAgICAgICAgIndzczovL2J1bGxldGluLXJwYy1qdXBpdGVyLmFwaS5wY2YubmV0d29yayIKICAgICAgXQogICAgfSwKICAgIHsKICAgICAgImNoYWluSWQiOiA0MjA0MjA0MTksCiAgICAgICJraW5kIjogImV0aC1ycGMiLAogICAgICAidXJscyI6IFsKICAgICAgICAiaHR0cHM6Ly9ldGgtcnBjLnBvbGthZG90LmlvIgogICAgICBdCiAgICB9LAogICAgewogICAgICAia2luZCI6ICJ3ZWItZ2F0ZXdheSIsCiAgICAgICJ1cmxzIjogWwogICAgICAgICJodHRwczovL2RvdC5saS8iCiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJraW5kIjogImV2bS1leHBsb3JlciIsCiAgICAgICJ1cmxzIjogWwogICAgICAgICJodHRwczovL2Jsb2Nrc2NvdXQucG9sa2Fkb3QuaW8vIgogICAgICBdCiAgICB9CiAgXSwKICAidGxkIjogImRvdCIsCiAgInZhbGlkVW50aWwiOiAxODAxMzA1Mjk5Cn0K","signature":"0x65820b1bf166da769b20ce44a80e22051b12ad13b30fa851f25dba26002cf2ada8a945b2f7a9cefda3a148f011dd14b494e0caa6d22ee6dbda327433c4a09c08"} \ No newline at end of file diff --git a/hosts/ios/polkadot-app/Modules/Products/TrUAPI/Stacks/trinity-stack-previewnet.json b/hosts/ios/polkadot-app/Modules/Products/TrUAPI/Stacks/trinity-stack-previewnet.json new file mode 100644 index 0000000000..372a3fc6e5 --- /dev/null +++ b/hosts/ios/polkadot-app/Modules/Products/TrUAPI/Stacks/trinity-stack-previewnet.json @@ -0,0 +1 @@ +{"$v":1,"payload":"ewogICIkdiI6IDEsCiAgImFzc2V0cyI6IFsKICAgIHsKICAgICAgImNoYWluIjogIkFzc2V0SHViIiwKICAgICAgImlkIjogNTAwMDA0MTMsCiAgICAgICJwYWxsZXQiOiAiQXNzZXRzIiwKICAgICAgInJvbGUiOiAicGF5bWVudCIKICAgIH0KICBdLAogICJjaGFpbnMiOiBbCiAgICB7CiAgICAgICJmdWxsU3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4NzkyNjliMGE1OTkxMDkzMGUwM2MyOGEwMmY2OTdmNWFkYzM1ZDMzZjY0NjgwZGNiYzZhZTMwNjc1ZjBjNGQwOSIsCiAgICAgICAgInVybCI6ICJodHRwczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9jaGFpbnNwZWNzL3Bhc2VvLWxvY2FsLmpzb24iCiAgICAgIH0sCiAgICAgICJnZW5lc2lzSGFzaCI6ICIweDg2MDE0NTc1MzY1N2U3M2MyOWI5Mzg4ZmZhMGE4YWViYzY0M2VhODc0MzRiNGIyNzFiNmMzYzNjYzllNmJmOTIiLAogICAgICAibGlnaHRTcGVjIjogewogICAgICAgICJzaGEyNTYiOiAiMHg4NTliNGY0MjdiODYyYjc0OGRjMDRiOWZmOGEwN2ZmNmRlNDAwZTZkYzUyYTc0MzA0YmQyNTdkZmUyYjhmYTEyIiwKICAgICAgICAidXJsIjogImh0dHBzOi8vcHJldmlld25ldC5zdWJzdHJhdGUuZGV2L2NoYWluc3BlY3MvcGFzZW8tbG9jYWwuc21vbC5qc29uIgogICAgICB9LAogICAgICAicm9sZSI6ICJSZWxheSIsCiAgICAgICJycGMiOiBbCiAgICAgICAgIndzczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9yZWxheS9hbGljZSIsCiAgICAgICAgIndzczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9yZWxheS9ib2IiLAogICAgICAgICJ3c3M6Ly9wcmV2aWV3bmV0LnN1YnN0cmF0ZS5kZXYvcmVsYXkvY2hhcmxpZSIsCiAgICAgICAgIndzczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9yZWxheS9kYXZlIiwKICAgICAgICAid3NzOi8vcHJldmlld25ldC5zdWJzdHJhdGUuZGV2L3JlbGF5L2V2ZSIsCiAgICAgICAgIndzczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9yZWxheS9mZXJkaWUiCiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJmdWxsU3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4Yjg1ZTVkYzhjZDU1MWUzNWU1YTVhNGVjNTM1NTc0YjBhMTdlNTg1YmNhMzljYWZkNGJmYzAyZDc4NjAyMjU2ZSIsCiAgICAgICAgInVybCI6ICJodHRwczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9jaGFpbnNwZWNzL2Fzc2V0LWh1Yi1sb2NhbC5qc29uIgogICAgICB9LAogICAgICAiZ2VuZXNpc0hhc2giOiAiMHhiYWM5N2UyM2ZjOGY0YmNjYWU3MmE5OGY4YWViMmJjYWIyMGJmNzU1ODYyMzA0ZTRiNDZhZDY0NzM0NTZlODk2IiwKICAgICAgImxpZ2h0U3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4ODljYWQ3YzE0NDdhZGRhM2QxZTk2MTM1NjA2OWFlMzAwMTM2M2Q3MGU5NmM1MGE3YWY5MGIyYWIwOGJlM2U1MSIsCiAgICAgICAgInVybCI6ICJodHRwczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9jaGFpbnNwZWNzL2Fzc2V0LWh1Yi1sb2NhbC5zbW9sLmpzb24iCiAgICAgIH0sCiAgICAgICJwYXJhSWQiOiAxNTAwLAogICAgICAicm9sZSI6ICJBc3NldEh1YiIsCiAgICAgICJycGMiOiBbCiAgICAgICAgIndzczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9hc3NldC1odWIiCiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJmdWxsU3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4YmU0ZDY2NjRlODhlM2Y1NWNjZDVmNWVmNWU0YTg1ZDhmY2MwZjY5OGFhMDQ0ZDUyZGJhOTZjYzA1N2Q4Y2ViZSIsCiAgICAgICAgInVybCI6ICJodHRwczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9jaGFpbnNwZWNzL2luZGl2aWR1YWxpdHktbG9jYWwuanNvbiIKICAgICAgfSwKICAgICAgImdlbmVzaXNIYXNoIjogIjB4NTVlM2U2ODllY2ZhOWQyZmZmY2Y3ZDMwOWI4MDExOTU2NjcxNDkzOTgyMjMwYmZkMDQyMGM2ODM1NDIyNDllOSIsCiAgICAgICJsaWdodFNwZWMiOiB7CiAgICAgICAgInNoYTI1NiI6ICIweGM1N2UxMzUwYjczNjM1NGQxMDg0Nzg1OTM5MDE3MDQwZjE4OTRhZDE2MzBlYmJmOTc1ZjNkMGQxMGQxOTM3OGEiLAogICAgICAgICJ1cmwiOiAiaHR0cHM6Ly9wcmV2aWV3bmV0LnN1YnN0cmF0ZS5kZXYvY2hhaW5zcGVjcy9pbmRpdmlkdWFsaXR5LWxvY2FsLnNtb2wuanNvbiIKICAgICAgfSwKICAgICAgInBhcmFJZCI6IDE1MDIsCiAgICAgICJyb2xlIjogIlBlb3BsZSIsCiAgICAgICJycGMiOiBbCiAgICAgICAgIndzczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9wZW9wbGUiCiAgICAgIF0sCiAgICAgICJzdGF0ZW1lbnRTdG9yZSI6IHRydWUKICAgIH0sCiAgICB7CiAgICAgICJmdWxsU3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4YWJlNWVlNTM1NmNiODJiZjQ3YmU5OWMzOTQ4NjhmNmMxMTBkMWI1ZmZlYTMzZjg0ZTc1MTA2OGQ4NDhiMTRmMSIsCiAgICAgICAgInVybCI6ICJodHRwczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9jaGFpbnNwZWNzL2J1bGxldGluLWxvY2FsLmpzb24iCiAgICAgIH0sCiAgICAgICJnZW5lc2lzSGFzaCI6ICIweGEwODExOTJiOTBjMWY2YTNmOGU5Y2U3YjJhODI0NmY0MWFmODA1YzY2NDU2Yzg0ZTA1ZmQ5N2MyYjM1MDI0MjUiLAogICAgICAibGlnaHRTcGVjIjogewogICAgICAgICJzaGEyNTYiOiAiMHhiMmE0Y2I1YjkyMDRiNmYxMDI0ZWQzY2I0MWU5ZTM0M2QwZWI2NGJmNzI1YWVhMDg0ZTliZTUyYjVjNjliMTJlIiwKICAgICAgICAidXJsIjogImh0dHBzOi8vcHJldmlld25ldC5zdWJzdHJhdGUuZGV2L2NoYWluc3BlY3MvYnVsbGV0aW4tbG9jYWwuc21vbC5qc29uIgogICAgICB9LAogICAgICAicGFyYUlkIjogMTUwMSwKICAgICAgInJvbGUiOiAiQnVsbGV0aW4iLAogICAgICAicnBjIjogWwogICAgICAgICJ3c3M6Ly9wcmV2aWV3bmV0LnN1YnN0cmF0ZS5kZXYvYnVsbGV0aW4iCiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJmdWxsU3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4Njg2NzAxNWE2OGQzOGEyNTkwNWJkYTM5ODVjZDFiMjUyMzc1ZDNkZDlmZTJlMmZlZmI3ZWVlMTlmZDRjNDI0OCIsCiAgICAgICAgInVybCI6ICJodHRwczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9jaGFpbnNwZWNzL3dlYjMtc3RvcmFnZS1sb2NhbC5qc29uIgogICAgICB9LAogICAgICAiZ2VuZXNpc0hhc2giOiAiMHg2NTdlYThlYmNjZjg3NmQ0MWFmNDQ4NzFkNzU4ODY2NDc5Y2RlMTAwMTM0YjU4ZTlkMDhlZDdlM2U0ZTYwMjgyIiwKICAgICAgImxpZ2h0U3BlYyI6IHsKICAgICAgICAic2hhMjU2IjogIjB4YWY5NDk0NGIwY2UzMmIwNDhhYmZjZTY2ZTI2NjhmNzA1NWNmMjhmMmZjNjQzMGQ4NTgyNDE2M2Y3YmFiMTMyYSIsCiAgICAgICAgInVybCI6ICJodHRwczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9jaGFpbnNwZWNzL3dlYjMtc3RvcmFnZS1sb2NhbC5zbW9sLmpzb24iCiAgICAgIH0sCiAgICAgICJwYXJhSWQiOiAxNjAwLAogICAgICAicm9sZSI6ICJXZWIzU3RvcmFnZSIsCiAgICAgICJycGMiOiBbCiAgICAgICAgIndzczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi93ZWIzLXN0b3JhZ2UiCiAgICAgIF0KICAgIH0KICBdLAogICJjb2luYWdlIjogewogICAgImNoYWluIjogIlBlb3BsZSIsCiAgICAiaW5zdGFuY2UiOiAwCiAgfSwKICAiZG90bnMiOiB7CiAgICAiY29udGVudFJlc29sdmVyIjogIjB4N2Y3NGQ3Y2Q1MGY1YTgzNDI3MGUyYWQzOTVhMDFiMDE4OTFhYjM3ZCIsCiAgICAiY29udHJvbGxlciI6ICIweGJkYWEwMWJkMWJhNjdkNzA5ZjJiMWZmMjg2ZGEwZDg1NDk3N2VhMzAiLAogICAgImNvc3RNb2RlbCI6ICIweDhiZmQxZjA5NTdlNzM3MTY3MzJlNzI1ODAyZjEzODMwYjU2ODJkYTQiLAogICAgImNyZWF0ZTNGYWN0b3J5IjogIjB4ODUzM2M3OWUwNThjNWE2NDg5Y2FmZWNhODZkYzYwMGUwMjlkNzVmNSIsCiAgICAibXVsdGljYWxsMyI6ICIweGI0NDY4MDAwYWJkODdkM2M1NmNiZmJkMTUzMTYxMjIzZDdiMTA5ZTUiLAogICAgIm5hbWVFc2Nyb3ciOiAiMHg0ODgxYWZiNzhlN2M5MDhjYWU4MTgxNjhiOTI2MjI5ZDkzMzc2NTIwIiwKICAgICJuYW1lV2hpdGVsaXN0IjogIjB4NDIwMTY2Y2Q2N2NhMDIzMzA5NGU0OTJhNGJiYTY3MDQ1ZWQ3YzM4YyIsCiAgICAicG9wQ29udHJvbGxlciI6ICIweGNjOTMyMzQ4NjA2Y2MxZjMzMThjYWRlYzVhNWNkMmNhNDQ3ZjhhNGIiLAogICAgInBvcExlbnMiOiAiMHhmZTVhNDVmN2ZkNThkMWE2ZmUwOTQ1NWRiNzk5NDA1YjFkY2U5NDExIiwKICAgICJwb3BSZXNvbHZlciI6ICIweGRhYzk4NDg4NGVjYThmYzQ0MDExZjFkNmM0OWIyNzgyODM5MGE3MmIiLAogICAgInBvcFJ1bGVzIjogIjB4NzQ3YjQ1NmJlMDNhZWMwYjQyYmQ4NWM1MTUxMzczMGZiZDQ1ZGEzMSIsCiAgICAicHJvdG9jb2xSZWdpc3RyeSI6ICIweGQxOWUzZDBjOTdjZjUwMTEyNWEwNGE5NzQwNWUzZTY1OTJmYTg0NmUiLAogICAgInJlZ2lzdHJhciI6ICIweDRmMDZlODE4YmEzZDk4NzcwNGZkOTFjZjNkODY4ZTRiMDE5MTA2YWIiLAogICAgInJlZ2lzdHJ5IjogIjB4ZjM0MDU0ZmQ3NmJiZjg1ZjIxNmNmOTkwODIyNmQ1ZjBhNzJlNTBjYSIsCiAgICAicmVzb2x2ZXIiOiAiMHhiZDExNjVlNTQ5ZGY5NmYwODNjMGExNmY2MTU5MDkyN2JjMTg3MDA5IiwKICAgICJyZXZlcnNlUmVzb2x2ZXIiOiAiMHhlZTM4ODNkN2ViNjBlZTliY2Q3ZjNiY2Q4ZjJmMDUzMDJhOWNjMDM1IiwKICAgICJzdG9yZUZhY3RvcnkiOiAiMHg5OTYwNWE5MjZmY2I0MGFiNTIwZjY1OWM2NTA1ZTVmZjg2Mjc3MWY2IgogIH0sCiAgImdlbmVyYXRpb24iOiAiMjAyNi0xMC0yNlQyMjoxOToyNS4zNTZaIiwKICAiaWQiOiAicHJldmlld25ldCIsCiAgIm5hbWUiOiAiUHJldmlld25ldCIsCiAgIm5ldHdvcmsiOiAicGFzZW8iLAogICJzZXF1ZW5jZSI6IDE3OTM3ODIyNDYsCiAgInNlcnZpY2VzIjogWwogICAgewogICAgICAia2luZCI6ICJkdWIiLAogICAgICAidXJscyI6IFsKICAgICAgICAiaHR0cHM6Ly9wcmV2aWV3bmV0LnN1YnN0cmF0ZS5kZXYvZHViLyIKICAgICAgXQogICAgfSwKICAgIHsKICAgICAgImtpbmQiOiAidHVybiIsCiAgICAgICJ1cmxzIjogWwogICAgICAgICJodHRwczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9kdWIvIgogICAgICBdCiAgICB9LAogICAgewogICAgICAia2luZCI6ICJzdHVuIiwKICAgICAgInVybHMiOiBbCiAgICAgICAgInN0dW46cHJldmlld25ldC5zdWJzdHJhdGUuZGV2OjM0NzgiCiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJraW5kIjogImlwZnMtZ2F0ZXdheSIsCiAgICAgICJ1cmxzIjogWwogICAgICAgICJodHRwczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9pcGZzLyIKICAgICAgXQogICAgfSwKICAgIHsKICAgICAgImtpbmQiOiAiaG9wIiwKICAgICAgInVybHMiOiBbCiAgICAgICAgIndzczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi9idWxsZXRpbiIKICAgICAgXQogICAgfSwKICAgIHsKICAgICAgImNoYWluSWQiOiA0MjA0MjA0MTcsCiAgICAgICJraW5kIjogImV0aC1ycGMiLAogICAgICAidXJscyI6IFsKICAgICAgICAiaHR0cHM6Ly9wcmV2aWV3bmV0LnN1YnN0cmF0ZS5kZXYvZXRoLXJwYyIKICAgICAgXQogICAgfSwKICAgIHsKICAgICAgImtpbmQiOiAic3RvcmFnZS1wcm92aWRlciIsCiAgICAgICJ1cmxzIjogWwogICAgICAgICJodHRwczovL3ByZXZpZXduZXQuc3Vic3RyYXRlLmRldi93ZWIzLXN0b3JhZ2UtcHJvdmlkZXIvIgogICAgICBdCiAgICB9LAogICAgewogICAgICAia2luZCI6ICJ3ZWItZ2F0ZXdheSIsCiAgICAgICJ1cmxzIjogWwogICAgICAgICJodHRwczovL3Rlc3RuZXQubGkvP25ldHdvcms9cHJldmlld25ldCIKICAgICAgXQogICAgfQogIF0sCiAgInRsZCI6ICJ0ZXN0bmV0Igp9Cg==","signature":"0x850dffc9be8cb6f5168a6ff25c0ceef282f08631d46ea645aeb14a07de96f9c6533a5584f608f4a7728b6172a70ea5e5ef764f72b469cd317a67d317ed652d0d"} \ No newline at end of file diff --git a/hosts/ios/polkadot-app/Modules/Products/TrUAPI/TrUAPIBundledStack.swift b/hosts/ios/polkadot-app/Modules/Products/TrUAPI/TrUAPIBundledStack.swift new file mode 100644 index 0000000000..b5c6ff46c8 --- /dev/null +++ b/hosts/ios/polkadot-app/Modules/Products/TrUAPI/TrUAPIBundledStack.swift @@ -0,0 +1,68 @@ +import Foundation +import TrUAPIHost + +/// The Trinity stacks the app bundles, and which one each build runs. The core +/// takes every chain it uses, the network it reports and the TLD the wallet's +/// reserved identities derive under from the running stack's descriptor. +/// +/// Each descriptor is a signed copy under `Stacks/`, kept byte for byte as +/// published. The core verifies it against its link's key when the runtime +/// starts, so a copy refreshed without its link stops the runtime. +enum TrUAPIBundledStack { + enum Error: Swift.Error, Equatable { + case missingDescriptor(resource: String) + } + + /// One stack the app bundles: its descriptor resource, the link it was + /// published under, and whether it may use Polkadot's genesis hashes and + /// the `dot` TLD, which only the mainnet build allows. + struct Definition { + let descriptorResource: String + let pointer: String + let allowsReservedValues: Bool + + /// The stack with its descriptor read from `bundle`, as the core takes it. + func load(from bundle: Bundle = .main) throws -> BundledStack { + guard let url = bundle.url(forResource: descriptorResource, withExtension: "json") else { + throw Error.missingDescriptor(resource: descriptorResource) + } + return try BundledStack( + pointer: pointer, + descriptor: Data(contentsOf: url), + allowsReservedValues: allowsReservedValues + ) + } + } + + static let previewnet = Definition( + descriptorResource: "trinity-stack-previewnet", + pointer: "polkadotapp://stack?key=0x4d43362f03d5511632a210632d1fb7760b5a663580f12ffca34619f79357e239" + + "&url=https%3A%2F%2Fpreviewnet.substrate.dev%2F.well-known%2Ftrinity-stack.json", + allowsReservedValues: false + ) + + static let paseoNextV2 = Definition( + descriptorResource: "trinity-stack-paseo-next-v2", + pointer: "polkadotapp://stack?key=0x2b89d8ca74aa2e11ab55f5af1168cd6929b88d1998aecd979b42cfc1078d5377" + + "&url=https%3A%2F%2Fstack-paseo-next.parity-testnet.parity.io%2F.well-known%2Ftrinity-stack.json", + allowsReservedValues: false + ) + + static let polkadot = Definition( + descriptorResource: "trinity-stack-polkadot", + pointer: "polkadotapp://stack?key=0xdcddb708759dade2e3850368ce0add2072f424ba53f1a2128f3339d646cffe28" + + "&url=https%3A%2F%2Fstack.api.pcf.network%2F.well-known%2Ftrinity-stack.json", + allowsReservedValues: true + ) + + /// The stack this build runs. + static var current: Definition { + #if UNSTABLE + previewnet + #elseif NIGHTLY || SAFETYNET + paseoNextV2 + #else + polkadot + #endif + } +} diff --git a/hosts/ios/polkadot-app/Modules/Products/TrUAPI/TrUAPIHostRuntimeProvider.swift b/hosts/ios/polkadot-app/Modules/Products/TrUAPI/TrUAPIHostRuntimeProvider.swift index 82eca5e92f..4516887613 100644 --- a/hosts/ios/polkadot-app/Modules/Products/TrUAPI/TrUAPIHostRuntimeProvider.swift +++ b/hosts/ios/polkadot-app/Modules/Products/TrUAPI/TrUAPIHostRuntimeProvider.swift @@ -10,11 +10,6 @@ import SubstrateSdk import KeyDerivation import Keystore_iOS -/// Runtime configuration error raised while assembling the shared host config. -enum TrUAPIRuntimeConfigError: Error { - case missingGenesisHash(chain: String) -} - /// Vends the process-wide ``TrUAPIHostRuntime``. Product executions open off /// the single shared runtime, so its authentication and core services are /// shared across every SPA and chat product. @@ -35,10 +30,10 @@ protocol TrUAPIHostRuntimeProviding: AnyObject, Sendable { func attach(workerManager: any TrUAPIWorkerManaging) } -/// Lazily builds one ``TrUAPIHostRuntime`` from host identity + people/bulletin -/// genesis hashes + the local session secret, activates the local session -/// once, and caches it. Lazy so startup is not blocked and the runtime is only -/// assembled once chains are synced and a session secret exists. +/// Lazily builds one ``TrUAPIHostRuntime`` from host identity + the stack the +/// build bundles + the local session secret, activates the local session once, +/// and caches it. Lazy so startup is not blocked and the runtime is only +/// assembled once a session secret exists. final class TrUAPIHostRuntimeProvider: TrUAPIHostRuntimeProviding, @unchecked Sendable { private let chainRegistry: ChainRegistryProtocol private let entropyManager: RootEntropyManaging @@ -135,7 +130,6 @@ final class TrUAPIHostRuntimeProvider: TrUAPIHostRuntimeProviding, @unchecked Se let secret = try entropyManager.fetchRootEntropy() let networkSuffix = try tldProvider.currentTldOrError() let runtimeConfig = try Self.makeRuntimeConfig( - chainRegistry: chainRegistry, secret: secret, liteUsername: settingsManager.string(for: .username), networkSuffix: networkSuffix, @@ -193,37 +187,16 @@ final class TrUAPIHostRuntimeProvider: TrUAPIHostRuntimeProviding, @unchecked Se } extension TrUAPIHostRuntimeProvider { - /// Assemble the immutable host-wide config. Genesis hashes are fetched from - /// the registry and must resolve; a missing hash fails explicitly rather - /// than degrading. `networkSuffix` is the dotNS TLD the core derives the - /// wallet's reserved identities under, so it has to be the one the app's own - /// built-in accounts derive from. Exposed for testing the genesis-validation - /// seam. + /// Assemble the immutable host-wide config. Every chain fact the core uses + /// comes from the stack this build bundles, ``TrUAPIBundledStack/current``. + /// `networkSuffix` is the dotNS TLD the app's own built-in accounts derive + /// from, which names the products backing the pinned Pocket cards. static func makeRuntimeConfig( - chainRegistry: ChainRegistryProtocol, secret: Data, liteUsername: String?, networkSuffix: String, databaseDirectory: String ) throws -> HostRuntimeConfig { - let peopleChain = try chainRegistry.getChainOrError(for: AppConfig.Chains.peopleChain) - let bulletinChain = try chainRegistry.getChainOrError(for: AppConfig.Chains.bulletInChain) - let assetHubChain = try chainRegistry.getChainOrError(for: AppConfig.Chains.assethubChain) - - guard let peopleGenesisHex = peopleChain.explicitGenesisHash else { - throw TrUAPIRuntimeConfigError.missingGenesisHash(chain: "people") - } - guard let bulletinGenesisHex = bulletinChain.explicitGenesisHash else { - throw TrUAPIRuntimeConfigError.missingGenesisHash(chain: "bulletin") - } - // Product manifests are read from the dotNS contracts on Asset Hub, so - // a missing hash here refuses every cross-product `trustedProducts` - // grant indistinguishably from the other product granting nothing. - // Fail explicitly, like its siblings, rather than passing all-zero. - guard let assetHubGenesisHex = assetHubChain.explicitGenesisHash else { - throw TrUAPIRuntimeConfigError.missingGenesisHash(chain: "assetHub") - } - let bundle = Bundle.main let version = bundle.appVersion.map { version in bundle.appBuild.map { "\(version) (\($0))" } ?? version @@ -234,10 +207,7 @@ extension TrUAPIHostRuntimeProvider { hostVersion: version, platformType: "ios", platformVersion: UIDevice.current.systemVersion, - peopleChainGenesisHash: Data(hexString: peopleGenesisHex), - bulletinChainGenesisHash: Data(hexString: bulletinGenesisHex), - assetHubChainGenesisHash: Data(hexString: assetHubGenesisHex), - networkSuffix: networkSuffix, + stack: TrUAPIBundledStack.current.load(), databaseDirectory: databaseDirectory, pinnedPocketCards: AssetPinnedPocketCards(tld: networkSuffix).cards(), localSessionSecret: secret, diff --git a/hosts/ios/polkadot-app/Modules/Products/TrUAPI/TrUAPISupportedChains.swift b/hosts/ios/polkadot-app/Modules/Products/TrUAPI/TrUAPISupportedChains.swift deleted file mode 100644 index 58f5925917..0000000000 --- a/hosts/ios/polkadot-app/Modules/Products/TrUAPI/TrUAPISupportedChains.swift +++ /dev/null @@ -1,43 +0,0 @@ -import Foundation -import TrUAPIHost -import ChainRegistry -import SubstrateSdk - -/// Builds the ``HostChainSet`` the rust core queries via `supportedChains()`: -/// the app's configured people, bulletin, and asset-hub chains, each resolved -/// to its genesis hash. Chains the registry cannot resolve are omitted so the -/// set matches exactly what `chainConnect` accepts. -enum TrUAPISupportedChains { - static func make(chainRegistry: ChainRegistryProtocol) -> HostChainSet { - let roles: [(identifier: ChainIdentifier, chainId: ChainModel.Id)] = [ - (.people, AppConfig.Chains.peopleChain), - (.bulletin, AppConfig.Chains.bulletInChain), - (.assetHub, AppConfig.Chains.assethubChain) - ] - - let chains = roles.compactMap { role -> HostChainEntry? in - guard - let chain = chainRegistry.getChain(for: role.chainId), - let genesisHex = chain.explicitGenesisHash, - let genesis = try? Data(hexString: genesisHex) - else { - return nil - } - return HostChainEntry(identifier: role.identifier, genesisHash: genesis) - } - - return HostChainSet(network: network, chains: chains) - } - - private static var network: String { - #if UNSTABLE - "polkadot" - #elseif SAFETYNET - "polkadot" - #elseif NIGHTLY - "paseo" - #else - "polkadot" - #endif - } -} diff --git a/hosts/ios/polkadot-appTests/TrUAPI/RustHostRuntimeBridgeTests.swift b/hosts/ios/polkadot-appTests/TrUAPI/RustHostRuntimeBridgeTests.swift index ddafa7a1e0..acfe2abc7b 100644 --- a/hosts/ios/polkadot-appTests/TrUAPI/RustHostRuntimeBridgeTests.swift +++ b/hosts/ios/polkadot-appTests/TrUAPI/RustHostRuntimeBridgeTests.swift @@ -135,18 +135,39 @@ struct RustHostRuntimeBridgeTests { // MARK: - Runtime config +private func makeDatabaseDirectory() throws -> String { + let directory = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString, isDirectory: true) + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + return directory.path +} + struct TrUAPIHostRuntimeProviderConfigTests { - /// Missing chains fail explicitly rather than degrading — the config seam - /// throws when the registry cannot resolve the required chains. - @Test func makeRuntimeConfigThrowsWhenChainsMissing() { - #expect(throws: (any Error).self) { - _ = try TrUAPIHostRuntimeProvider.makeRuntimeConfig( - chainRegistry: MockChainRegistry(), - secret: Data([0x01]), - liteUsername: nil, - networkSuffix: "paseo", - databaseDirectory: NSTemporaryDirectory() - ) - } + /// The core verifies the bundled descriptor under the build's stack link + /// before it starts, so a config the core refuses leaves the app with no + /// runtime and no product can open. + @Test func runtimeStartsFromTheAssembledConfig() throws { + let config = try TrUAPIHostRuntimeProvider.makeRuntimeConfig( + secret: Data(repeating: 7, count: 32), + liteUsername: nil, + networkSuffix: "testnet", + databaseDirectory: makeDatabaseDirectory() + ) + + _ = try TrUAPIHostRuntime(bridge: makeHostBridge(), runtimeConfig: config) + } + + /// Tests run in one build only, so the stacks the other builds run are + /// started here as well: a copy refreshed without its link, or a + /// reserved-values flag that refuses its stack, fails before it ships. + @Test(arguments: [TrUAPIBundledStack.previewnet, TrUAPIBundledStack.paseoNextV2, TrUAPIBundledStack.polkadot]) + func everyBundledStackStartsTheCore(stack: TrUAPIBundledStack.Definition) throws { + let config = try HostRuntimeConfig( + hostName: "Polkadot App", + stack: stack.load(), + databaseDirectory: makeDatabaseDirectory() + ) + + _ = try TrUAPIHostRuntime(bridge: makeHostBridge(), runtimeConfig: config) } } diff --git a/ios/truapi-host/README.md b/ios/truapi-host/README.md index 678b635d4c..4d9eabf49c 100644 --- a/ios/truapi-host/README.md +++ b/ios/truapi-host/README.md @@ -73,7 +73,7 @@ Run `rebuild.sh` after changing anything host-visible — the `NativeTrUApiHostR For local iteration without publishing, set `TRUAPI_USE_LOCAL_BINARY=1` so the root `Package.swift` builds against `Binaries/` directly. -The embedding app implements `HostBridge` (defined in `TrUAPIHost.swift`): navigation, push, permissions, auth state, scoped + core storage, chain JSON-RPC, confirmations, preimage, theme, feature support, and the served chain set. UI-decision callbacks are `async` and awaited by the Rust core. `HostCallbackAdapter` translates it to the UniFFI-generated `HostCallbacks` protocol; `TrUAPIHostRuntime` and each product execution retain their own adapter. Conform to `HostBridge` rather than to the generated protocol: its extension defaults the optional callbacks, so a newly added one does not break the build. Storage arrives as the `storage` and `coreStorage` sub-objects, which the adapter flattens. +The embedding app implements `HostBridge` (defined in `TrUAPIHost.swift`): navigation, push, permissions, auth state, scoped + core storage, chain JSON-RPC, confirmations, preimage, theme, and feature support. UI-decision callbacks are `async` and awaited by the Rust core. `HostCallbackAdapter` translates it to the UniFFI-generated `HostCallbacks` protocol; `TrUAPIHostRuntime` and each product execution retain their own adapter. Conform to `HostBridge` rather than to the generated protocol: its extension defaults the optional callbacks, so a newly added one does not break the build. Storage arrives as the `storage` and `coreStorage` sub-objects, which the adapter flattens. ## Integrating in an iOS app @@ -94,20 +94,22 @@ manifest PR to keep `main` current. SPM pins the resolved revision in the app's `Package.resolved`; update it with File > Packages > Update in Xcode or `xcodebuild -resolvePackageDependencies` after the tag is published. -`HostRuntimeConfig.networkSuffix` is required. Supply the bare TLD (`dot`, -`paseo`, or `testnet`) from the same network configuration used by onboarding -and the People/Bulletin genesis hashes. It must match the People chain's -`NetworkSuffix.NetworkSuffix`. Include this configuration update in the -embedding app's package upgrade. - -`HostRuntimeConfig.assetHubChainGenesisHash` is required. Supply the Asset Hub -genesis hash from the same network configuration, as 32 bytes. Product manifests -are read from the dotNS contracts deployed there, so it is what makes a -`trustedProducts` grant resolvable: without a usable value no manifest resolves, -so every cross-product grant not already cached is refused, and the refusal is -indistinguishable from the other product having granted nothing. Pass 32 zero -bytes only to declare deliberately that this host has no Asset Hub. Include this -configuration update in the embedding app's package upgrade. +`HostRuntimeConfig.stack` is required. It is the Trinity stack the build runs, +as a `BundledStack`: + +- `pointer` is the stack's link, `polkadotapp://stack?key=0x…&url=…`. It must + carry a key. +- `descriptor` is the signed descriptor the build bundles, byte for byte as + served. The core verifies it under the pointer's key. +- `allowsReservedValues` is true only in a Polkadot build. Without it the core + refuses a stack that uses Polkadot's genesis hashes or the `dot` TLD. + +The core takes every chain it uses from the descriptor, and answers products' +chain info queries from its `chains` and `network`. The descriptor's TLD is the +one the wallet's reserved identities derive under. An invalid stack makes +`TrUAPIHostRuntime(bridge:runtimeConfig:)` throw +`NativeRuntimeConfigError.Invalid(reason:)`. Include this configuration update +in the embedding app's package upgrade. Run the package tests in their UIKit host on an iOS simulator (the xcframework has no macOS slice). The helper installs pinned XcodeGen under `.agent/tools`, generates the project, and selects an available simulator: @@ -163,10 +165,7 @@ let runtime = try TrUAPIHostRuntime( bridge: bridge, runtimeConfig: HostRuntimeConfig( hostName: "My Chat Host", - peopleChainGenesisHash: peopleChainGenesisHash, // exactly 32 bytes - bulletinChainGenesisHash: bulletinChainGenesisHash, - assetHubChainGenesisHash: assetHubChainGenesisHash, - networkSuffix: "dot" + stack: bundledStack ) ) // Chat needs an active session; without one every Chat call answers denied. @@ -604,16 +603,16 @@ final class MyBridge: HostBridge, @unchecked Sendable { } let bridge = MyBridge() +let descriptorURL = Bundle.main.url(forResource: "trinity-stack", withExtension: "json")! let runtimeConfig = HostRuntimeConfig( hostName: "My Host", hostIcon: "https://host.example/icon.png", - peopleChainGenesisHash: Data(repeating: 0, count: 32), - bulletinChainGenesisHash: Data(repeating: 0, count: 32), - // Stand-in for a real Asset Hub genesis hash. Non-zero on purpose: - // all-zero is the "no Asset Hub" sentinel and refuses every cross-product - // `trustedProducts` grant. - assetHubChainGenesisHash: Data(repeating: 1, count: 32), - networkSuffix: "dot" + stack: BundledStack( + // The link the bundled descriptor was published under. + pointer: "polkadotapp://stack?key=0x…&url=…", + descriptor: try Data(contentsOf: descriptorURL), + allowsReservedValues: false + ) ) let runtime = try TrUAPIHostRuntime(bridge: bridge, runtimeConfig: runtimeConfig) try runtime.activateLocalSession(secret: entropyBytes, liteUsername: nil) diff --git a/ios/truapi-host/Sources/TrUAPIHost/TrUAPIHost.swift b/ios/truapi-host/Sources/TrUAPIHost/TrUAPIHost.swift index 2876e00985..0a19592f96 100644 --- a/ios/truapi-host/Sources/TrUAPIHost/TrUAPIHost.swift +++ b/ios/truapi-host/Sources/TrUAPIHost/TrUAPIHost.swift @@ -151,10 +151,6 @@ public protocol HostBridge: AnyObject, Sendable { /// return promptly. func featureSupported(request: HostFeatureSupportedRequest) async throws -> Bool - /// Enumerate the chains this host serves: its environment plus one entry - /// per chain role. Invoked on the dispatcher thread; must return promptly. - func supportedChains() throws -> HostChainSet - /// The chains this host builds as V5 general transactions, each with its /// transaction extension pipeline; every other chain is built as V4. The /// core builds its own extrinsics the same way. Defaults to none. @@ -422,7 +418,6 @@ public extension HostBridge { languageTag: Locale.current.language.languageCode?.identifier ?? "en" ) } - func supportedChains() throws -> HostChainSet { HostChainSet(network: "", chains: []) } func transactionExtensionVersions() async throws -> ExtensionVersionMap { ExtensionVersionMap(chains: []) } @@ -842,12 +837,6 @@ private final class HostCallbackAdapter: HostCallbacks, @unchecked Sendable { } } - func supportedChains() throws -> HostChainSet { - try withHostRejection { - try bridge.supportedChains() - } - } - func transactionExtensionVersions() async throws -> ExtensionVersionMap { try await withHostRejection { try await bridge.transactionExtensionVersions() diff --git a/ios/truapi-host/Tests/ProductNetworkAccessTests.swift b/ios/truapi-host/Tests/ProductNetworkAccessTests.swift index 7252122598..a585d57712 100644 --- a/ios/truapi-host/Tests/ProductNetworkAccessTests.swift +++ b/ios/truapi-host/Tests/ProductNetworkAccessTests.swift @@ -275,9 +275,7 @@ private struct NetworkTestProduct { let server = try await NetworkTestServer.start() do { let runtime = try TrUAPIHostRuntime(bridge: bridge, runtimeConfig: HostRuntimeConfig( - hostName: "network-tests", peopleChainGenesisHash: Data(repeating: 0, count: 32), - bulletinChainGenesisHash: Data(repeating: 0, count: 32), - assetHubChainGenesisHash: Data(repeating: 1, count: 32), networkSuffix: "paseo", + hostName: "network-tests", stack: previewnetStack(), databaseDirectory: temporaryDatabaseDirectory() )) let execution = try runtime.openProductExecution( diff --git a/ios/truapi-host/Tests/TrUAPIWsBridgeTests.swift b/ios/truapi-host/Tests/TrUAPIWsBridgeTests.swift index b0e127cab2..2dbdfd93c2 100644 --- a/ios/truapi-host/Tests/TrUAPIWsBridgeTests.swift +++ b/ios/truapi-host/Tests/TrUAPIWsBridgeTests.swift @@ -153,12 +153,7 @@ private extension TrUAPIWsBridgeTests { static func makeHostRuntimeConfig() throws -> HostRuntimeConfig { try HostRuntimeConfig( hostName: "truapi-host-tests", - peopleChainGenesisHash: Data(repeating: 0, count: 32), - bulletinChainGenesisHash: Data(repeating: 0, count: 32), - // Non-zero: all-zero is the "no Asset Hub" sentinel, and this - // fixture is not exercising that case. - assetHubChainGenesisHash: Data(repeating: 1, count: 32), - networkSuffix: "paseo", + stack: previewnetStack(), databaseDirectory: temporaryDatabaseDirectory() ) } @@ -229,6 +224,24 @@ final class StubCoreStorage: HostCoreStorageBackend, @unchecked Sendable { } } +/// The previewnet stack the CLI bundles, read from its signed copies in this +/// checkout, so the core verifies a real descriptor against its link. +/// +/// The tests run on a simulator, which reads the host file system, so the +/// path comes from this file's location rather than a test bundle resource. +func previewnetStack() throws -> BundledStack { + let stacks = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() + .appendingPathComponent("../../../rust/crates/truapi-host-cli/stacks") + .standardizedFileURL + let link = try String(contentsOf: stacks.appendingPathComponent("previewnet.link"), encoding: .utf8) + return try BundledStack( + pointer: link.trimmingCharacters(in: .whitespacesAndNewlines), + descriptor: Data(contentsOf: stacks.appendingPathComponent("previewnet.json")), + allowsReservedValues: false + ) +} + // Conforms to HostBridge rather than the generated HostCallbacks, so the // protocol extension supplies every optional callback and a new one cannot // leave this file behind. Only the six requirements without a default are @@ -293,7 +306,6 @@ final class StubHostBridge: HostBridge, @unchecked Sendable { request _: RemotePermission ) async throws -> PermissionDecision { nextRemoteDecision() } func featureSupported(request _: HostFeatureSupportedRequest) async throws -> Bool { true } - func supportedChains() throws -> HostChainSet { HostChainSet(network: "", chains: []) } func localStorageRead(key: String) throws -> Data? { try storage.read(key: key) } func localStorageWrite(key: String, value: Data) throws { diff --git a/js/packages/truapi-host/README.md b/js/packages/truapi-host/README.md index 1cc065d9cb..adc4819544 100644 --- a/js/packages/truapi-host/README.md +++ b/js/packages/truapi-host/README.md @@ -27,20 +27,22 @@ The package exposes tree-shakeable subpath exports — import only what your env `WasmSigningHostRuntime`; `wasm/testing` adds the Rust `wasm-signing-host` and `test-host` features, which is what lets the test host hold keys and answer resource allocation as granted without allocating anything. -`ProductRuntimeConfig` configures the pairing host and requires no network -suffix. The signing constructor's configuration requires -`runtimeConfig.networkSuffix` in addition: the bare TLD (`dot`, `paseo`, or -`testnet`) matching the People chain and the wallet's onboarding -configuration. - -`runtimeConfig.assetHub` is required by both configurations, pairing and -signing. It is the Asset Hub genesis hash, in the same shape as -`runtimeConfig.people` and `runtimeConfig.bulletin`. Product manifests are read -from the dotNS contracts deployed there, so it is what makes a -`trustedProducts` grant resolvable: without a usable value no manifest resolves, -so every cross-product grant not already cached is refused, and the refusal is -indistinguishable from the other product having granted nothing. A config -omitting it is rejected. +`ProductRuntimeConfig` configures both host roles. Its `stack` is the Trinity +stack the host build runs: `pointer`, the stack's link +(`polkadotapp://stack?key=0x…&url=…`), and `descriptor`, the bytes of the +signed copy the build bundles, which the core verifies against the pointer's +key. Every chain the core uses, the network `getChainInfo` reports, and the TLD +a signing host derives its reserved identities under come from that +descriptor. The core refuses a pointer without a key, a descriptor without a +People or Bulletin chain, and a stack that uses Polkadot's genesis hashes or +the `dot` TLD unless `allowsReservedValues` is set, which only a Polkadot build +does. A stack without an Asset Hub resolves no product manifest from dotNS, so +every cross-product grant not already cached is refused. + +The test host has no descriptor to bundle, so it passes `testStack` in place of +`stack`: the network, the TLD and each chain's genesis hash. Only the +`wasm/testing` bundle reads it. `mockRuntimeConfig` fills it with the +`MOCK_GENESIS` chains, and the Playwright fixture builds it from `networks`. ## Bundler requirements diff --git a/js/packages/truapi-host/src/runtime.ts b/js/packages/truapi-host/src/runtime.ts index 32d71bd620..6f9ff3816e 100644 --- a/js/packages/truapi-host/src/runtime.ts +++ b/js/packages/truapi-host/src/runtime.ts @@ -60,6 +60,26 @@ export interface ChainConnection { */ export type LogLevel = string; +/** The Trinity stack a host build bundles. */ +export interface BundledStack { + /** + * The stack's link, `polkadotapp://stack?key=0x…&url=…`. It must carry a key: + * the core refuses an unsigned stack. + */ + pointer: string; + /** + * The bundled descriptor's bytes as served: a signed envelope that verifies + * under the pointer's key. + */ + descriptor: Uint8Array; + /** + * Whether this build may run a stack that uses Polkadot's genesis hashes or + * the `dot` TLD. Only a Polkadot or Polkadot-fork build sets it. Defaults to + * `false`. + */ + allowsReservedValues?: boolean; +} + /** Configuration for one product runtime hosted by the wasm core. */ export interface ProductRuntimeConfig { /** Stable identifier used to scope product accounts, permissions, and storage. */ @@ -87,45 +107,17 @@ export interface ProductRuntimeConfig { /** Platform or operating-system version. */ version?: string; }; - /** People-chain configuration used for statement-store SSO. */ - people: { - /** People-chain genesis hash. */ - genesisHash: string | Uint8Array; - }; - /** Bulletin-chain configuration used for in-core preimage submission. */ - bulletin: { - /** Bulletin-chain genesis hash. */ - genesisHash: string | Uint8Array; - }; /** - * Asset Hub configuration. Session usernames and the product manifests - * that carry `trustedProducts` grants are both read from the dotNS - * contracts deployed there. Without a usable genesis hash no manifest - * resolves, so every cross-product grant not already cached is refused, - * indistinguishably from the other product having granted nothing. An - * all-zero hash declares deliberately that this host has no Asset Hub. - * - * The wasm signing host requires the same `assetHub.genesisHash`, though it - * takes an untyped config object rather than this interface. + * The stack the host build runs. Every chain the core uses, the network + * `get_chain_info` reports, and the dotNS TLD a signing host derives its + * reserved identities under all come from its descriptor. */ - assetHub: { - /** Asset Hub genesis hash. */ - genesisHash: string | Uint8Array; - }; + stack: BundledStack; /** Wallet pairing configuration. */ pairing: { /** URI scheme used for wallet pairing deeplinks. */ deeplinkScheme: string; }; - /** - * dotNS TLD the host's reserved identities derive under, such as `"dot"` or - * `"paseo"`. - * - * Required by a signing host, which derives its own keys and so has to know - * which network's identities it is deriving. A pairing host never derives - * them -- the paired wallet does -- and ignores this. - */ - networkSuffix?: string; } /** diff --git a/js/packages/truapi-host/src/test-support.ts b/js/packages/truapi-host/src/test-support.ts index 647f341599..1c7cf21102 100644 --- a/js/packages/truapi-host/src/test-support.ts +++ b/js/packages/truapi-host/src/test-support.ts @@ -1,4 +1,7 @@ +import { readFileSync } from "node:fs"; + import type { RequiredHostCallbacks } from "./generated/host-callbacks.js"; +import type { BundledStack } from "./runtime.js"; /** `HostCallbacks` with every optional member required, for exhaustive test fixtures. */ export type CompleteHostCallbacks = RequiredHostCallbacks; @@ -27,7 +30,6 @@ export function makeHostCallbacks( }, features: { featureSupported: async () => ({ supported: false }), - supportedChains: async () => ({ network: "paseo", chains: [] }), }, extrinsicVersion: { transactionExtensionVersions: async () => ({ chains: [] }), @@ -165,3 +167,20 @@ export function makeHostCallbacks( export function settle(): Promise { return new Promise((resolve) => setImmediate(resolve)); } + +/** + * The previewnet stack as a host build bundles it: the signed copy and link + * the CLI ships, read from `rust/crates/truapi-host-cli/stacks`. + */ +export function bundledTestStack(): BundledStack { + const stacks = new URL( + "../../../../rust/crates/truapi-host-cli/stacks/", + import.meta.url, + ); + return { + pointer: readFileSync(new URL("previewnet.link", stacks), "utf8").trim(), + descriptor: new Uint8Array( + readFileSync(new URL("previewnet.json", stacks)), + ), + }; +} diff --git a/js/packages/truapi-host/src/testing.ts b/js/packages/truapi-host/src/testing.ts index d5af0edd28..089da0a7e1 100644 --- a/js/packages/truapi-host/src/testing.ts +++ b/js/packages/truapi-host/src/testing.ts @@ -7,6 +7,7 @@ export { createMockHost, mockRuntimeConfig, + mockTestStack, MOCK_GENESIS, } from "./web/create-mock-host.js"; export type { @@ -15,8 +16,11 @@ export type { MockFaults, MockHost, MockHostConfig, + MockRuntimeConfig, PermissionLogEntry, PermissionKind, PermissionPolicy, SigningLogEntry, + TestStack, + TestStackChain, } from "./web/create-mock-host.js"; diff --git a/js/packages/truapi-host/src/testing/dev-accounts.ts b/js/packages/truapi-host/src/testing/dev-accounts.ts index 2b0b0fef38..c24c849d60 100644 --- a/js/packages/truapi-host/src/testing/dev-accounts.ts +++ b/js/packages/truapi-host/src/testing/dev-accounts.ts @@ -13,6 +13,8 @@ // pin addresses by reading them back from the host, never by hard-coding a // well-known one. +import { mockTestStack, type TestStack } from "../web/create-mock-host.js"; + /** Name of a built-in dev account. */ export type DevAccountName = "alice" | "bob" | "charlie" | "dave"; @@ -144,8 +146,8 @@ export const DEFAULT_CHAIN = PASEO_ASSET_HUB; /** * Fixture settings for proxying one real chain. * - * A live chain has to agree in three places -- what the host proxies to, what - * it reports serving, and what its runtime config declares -- and the product + * A live chain has to agree in three places: what the host proxies to, what + * it reports serving, and the Asset Hub of the test stack it runs. The product * checks the last two against its own descriptor bundle. Getting one of them * wrong fails as a genesis mismatch that names neither the setting nor the * file, so this builds all three from one value. @@ -161,25 +163,24 @@ export const DEFAULT_CHAIN = PASEO_ASSET_HUB; export function liveChain(chain: { rpcUrl: string; genesisHash: string }): { mock: { chainProxies: { rpcUrl: string }[]; - supportedChains: { - network: string; - chains: { identifier: "AssetHub"; genesisHash: `0x${string}` }[]; - }; + servedChains: string[]; }; - runtimeConfig: Record; + runtimeConfig: { testStack: TestStack }; } { - const genesisHash = chain.genesisHash as `0x${string}`; return { mock: { // No hash on the proxy: it takes every request, so routing survives a // reset even while the declared hash below has to be re-pinned. chainProxies: [{ rpcUrl: chain.rpcUrl }], - supportedChains: { + servedChains: [chain.genesisHash], + }, + runtimeConfig: { + testStack: { + ...mockTestStack(), network: "paseo", - chains: [{ identifier: "AssetHub", genesisHash }], + assetHub: { genesisHash: chain.genesisHash }, }, }, - runtimeConfig: { assetHub: { genesisHash } }, }; } diff --git a/js/packages/truapi-host/src/testing/fixture-compat.test.ts b/js/packages/truapi-host/src/testing/fixture-compat.test.ts index 75144b4013..34e93fb658 100644 --- a/js/packages/truapi-host/src/testing/fixture-compat.test.ts +++ b/js/packages/truapi-host/src/testing/fixture-compat.test.ts @@ -11,6 +11,7 @@ import { import { createMockClient } from "./create-mock-client.js"; import { wasmIsBuilt } from "./require-wasm.js"; import { PASEO_ASSET_HUB, LIVE_CHAINS } from "./dev-accounts.js"; +import { mockTestStack } from "../web/create-mock-host.js"; const SAMPLE_CHAIN = { id: "paseo-asset-hub", @@ -49,29 +50,40 @@ async function hostPageUrlFor( } describe("host-api-test-sdk option compatibility", () => { - it("expands `networks` into the three settings that must agree", () => { - const { mock, runtimeConfig } = fromNetworks([SAMPLE_CHAIN]); - expect(mock.supportedChains).toEqual({ - network: "paseo", - chains: [{ identifier: "AssetHub", genesisHash: "0x23e730eb" }], - }); + it("expands `networks` into the settings that must agree", () => { // Unhashed on purpose: an unhashed proxy takes every request, so routing - // survives a chain reset even when the declared hash goes stale. - expect(mock.chainProxies).toEqual([{ rpcUrl: SAMPLE_CHAIN.rpcUrl }]); - expect(runtimeConfig).toEqual({ assetHub: { genesisHash: "0x23e730eb" } }); + // survives a chain reset even when the declared hash goes stale. The test + // stack keeps the mock People and Bulletin, which the core needs to start. + expect(fromNetworks([SAMPLE_CHAIN])).toEqual({ + mock: { + chainProxies: [{ rpcUrl: SAMPLE_CHAIN.rpcUrl }], + servedChains: ["0x23e730eb"], + }, + runtimeConfig: { + testStack: { + ...mockTestStack(), + network: "paseo", + assetHub: { genesisHash: "0x23e730eb" }, + }, + }, + }); }); it("reads the chain's role from the id suffix", () => { - expect( - fromNetworks([{ ...SAMPLE_CHAIN, id: "paseo-people" }]).runtimeConfig, - ).toEqual({ people: { genesisHash: "0x23e730eb" } }); - const relay = fromNetworks([{ ...SAMPLE_CHAIN, id: "previewnet" }]); - expect(relay.mock.supportedChains).toEqual({ - network: "previewnet", - chains: [{ identifier: "Relay", genesisHash: "0x23e730eb" }], - }); - // The runtime config has no relay slot, so there is nothing to declare. - expect(relay.runtimeConfig).toEqual({}); + const stackOf = (id: string) => + fromNetworks([{ ...SAMPLE_CHAIN, id }]).runtimeConfig.testStack; + expect([stackOf("paseo-people"), stackOf("previewnet")]).toEqual([ + { + ...mockTestStack(), + network: "paseo", + people: { genesisHash: "0x23e730eb" }, + }, + { + ...mockTestStack(), + network: "previewnet", + relay: { genesisHash: "0x23e730eb" }, + }, + ]); }); it("rejects a pinned product account, and says why", () => { @@ -104,8 +116,8 @@ describe("host-api-test-sdk option compatibility", () => { it("exports PASEO_ASSET_HUB, carrying the hash we actually proxy to", () => { expect(PASEO_ASSET_HUB.id).toBe("paseo-asset-hub"); const expanded = fromNetworks([PASEO_ASSET_HUB]); - expect(expanded.runtimeConfig).toEqual({ - assetHub: { genesisHash: LIVE_CHAINS.paseoAssetHub.genesisHash }, + expect(expanded.runtimeConfig.testStack.assetHub).toEqual({ + genesisHash: LIVE_CHAINS.paseoAssetHub.genesisHash, }); }); diff --git a/js/packages/truapi-host/src/testing/networks.ts b/js/packages/truapi-host/src/testing/networks.ts index 55ff9bff7e..081dfabc66 100644 --- a/js/packages/truapi-host/src/testing/networks.ts +++ b/js/packages/truapi-host/src/testing/networks.ts @@ -3,7 +3,11 @@ // by the Playwright fixture and the node server, so a suite gets the same // chains whichever one it configures. -import type { MockHostConfig } from "../web/create-mock-host.js"; +import { + mockTestStack, + type MockHostConfig, + type TestStack, +} from "../web/create-mock-host.js"; /** One chain, in `@parity/host-api-test-sdk`'s shape. */ export interface NetworkConfig { @@ -20,7 +24,7 @@ export interface NetworkConfig { function splitChainId(id: string): { network: string; identifier: "AssetHub" | "People" | "Bulletin" | "Relay"; - configKey?: "assetHub" | "people" | "bulletin"; + stackKey: "assetHub" | "people" | "bulletin" | "relay"; } { const suffixes = [ ["-asset-hub", "AssetHub", "assetHub"], @@ -29,12 +33,15 @@ function splitChainId(id: string): { ] as const; for (const [suffix, identifier, configKey] of suffixes) { if (id.endsWith(suffix)) { - return { network: id.slice(0, -suffix.length), identifier, configKey }; + return { + network: id.slice(0, -suffix.length), + identifier, + stackKey: configKey, + }; } } - // No suffix: the id names the network and the chain is its relay. The - // runtime config has no relay slot, so there is no key to declare it under. - return { network: id, identifier: "Relay" }; + // No suffix: the id names the network and the chain is its relay. + return { network: id, identifier: "Relay", stackKey: "relay" }; } /** @@ -48,7 +55,11 @@ function splitChainId(id: string): { export type LoopbackStatements = boolean | "default"; /** - * Expand `networks` into the three settings that have to agree. + * Expand `networks` into the settings that have to agree: the proxies that + * route each chain, the chains `featureSupported` reports served, and the test + * stack the core answers `getChainInfo` from. A role `networks` leaves out + * keeps the {@link mockTestStack} chain, because the core needs People and + * Bulletin to start. * * A single proxy carries no genesis hash: an unhashed proxy takes every * request, so routing survives a chain reset while only the DECLARED hash -- @@ -63,22 +74,14 @@ export function fromNetworks( networks: NetworkConfig[], loopbackStatements: LoopbackStatements = false, ): { - mock: Pick; - runtimeConfig: Record; + mock: Pick; + runtimeConfig: { testStack: TestStack }; } { - const runtimeConfig: Record = {}; - const chains: { identifier: string; genesisHash: string }[] = []; - let network = "paseo"; + const testStack: TestStack = { ...mockTestStack(), network: "paseo" }; for (const entry of networks) { const split = splitChainId(entry.id); - network = split.network || network; - chains.push({ - identifier: split.identifier, - genesisHash: entry.genesisHash, - }); - if (split.configKey) { - runtimeConfig[split.configKey] = { genesisHash: entry.genesisHash }; - } + testStack.network = split.network || testStack.network; + testStack[split.stackKey] = { genesisHash: entry.genesisHash }; } // Only the People chain carries the statement store, so serving it locally on // the hub as well would claim a store where none exists. A suite that declares @@ -117,9 +120,9 @@ export function fromNetworks( rpcUrl: entry.rpcUrl, ...(servesStatements(entry) ? { loopbackStatements: true } : {}), })), - supportedChains: { network, chains }, - } as Pick, - runtimeConfig, + servedChains: networks.map((entry) => entry.genesisHash), + }, + runtimeConfig: { testStack }, }; } diff --git a/js/packages/truapi-host/src/testing/playwright.ts b/js/packages/truapi-host/src/testing/playwright.ts index 1fe76f2a21..ab06d32017 100644 --- a/js/packages/truapi-host/src/testing/playwright.ts +++ b/js/packages/truapi-host/src/testing/playwright.ts @@ -96,8 +96,8 @@ export interface TestHostFixtureOptions { /** * Chains to serve, in `@parity/host-api-test-sdk`'s `NetworkConfig` shape. * - * A convenience over spelling out `mock.chainProxies`, `mock.supportedChains` - * and `runtimeConfig` separately, which have to agree. The chain's role is + * A convenience over spelling out `mock.chainProxies`, `mock.servedChains` + * and `runtimeConfig.testStack` separately, which have to agree. The chain's role is * read from the `id` suffix (`-asset-hub`, `-people`, `-bulletin`, else the * relay) and the rest of the id is the network name. * diff --git a/js/packages/truapi-host/src/testing/server-config.test.ts b/js/packages/truapi-host/src/testing/server-config.test.ts index c1a7e925a9..5b93cfb565 100644 --- a/js/packages/truapi-host/src/testing/server-config.test.ts +++ b/js/packages/truapi-host/src/testing/server-config.test.ts @@ -2,6 +2,7 @@ import { asProductId } from "@parity/truapi"; import { describe, expect, it } from "bun:test"; import { deepLinkUrl, hostPageUrl, productUrlFor } from "./host-page-url.js"; import { createTestHostServer } from "./server.js"; +import { mockTestStack } from "../web/create-mock-host.js"; describe("createTestHostServer host configuration", () => { it("returns a URL carrying the configuration it was given", async () => { @@ -44,13 +45,14 @@ describe("createTestHostServer host configuration", () => { const url = new URL(server.url); expect(JSON.parse(url.searchParams.get("mock")!)).toMatchObject({ chainProxies: [{ rpcUrl: "wss://hub.invalid" }], - supportedChains: { - network: "paseo", - chains: [{ identifier: "AssetHub", genesisHash }], - }, + servedChains: [genesisHash], }); expect(JSON.parse(url.searchParams.get("runtimeConfig")!)).toEqual({ - assetHub: { genesisHash }, + testStack: { + ...mockTestStack(), + network: "paseo", + assetHub: { genesisHash }, + }, }); await server.close(); }); @@ -72,7 +74,7 @@ describe("createTestHostServer host configuration", () => { JSON.parse(new URL(server.url).searchParams.get("mock")!), ).toMatchObject({ chainProxies: [], - supportedChains: { network: "paseo" }, + servedChains: [`0x${"aa".repeat(32)}`], }); await server.close(); }); diff --git a/js/packages/truapi-host/src/web/create-mock-host.test.ts b/js/packages/truapi-host/src/web/create-mock-host.test.ts index 48ea349ff3..893f930c04 100644 --- a/js/packages/truapi-host/src/web/create-mock-host.test.ts +++ b/js/packages/truapi-host/src/web/create-mock-host.test.ts @@ -659,14 +659,22 @@ describe("createMockHost TestHostAPI parity", () => { // The Rust mock declares the same three. A suite that routes on a chain // has to be answered the same way by either, and dropping one here would // otherwise only show up as a chain-routed call failing much later. - const host = createMockHost(); - expect(await host.callbacks.features.supportedChains()).toEqual({ + const host = createMockHost({ onlyServedChains: true }); + const supported = (genesisHash: string) => + host.callbacks.features + .featureSupported({ tag: "Chain", value: { genesisHash } }) + .then((answer) => answer.supported); + expect( + await Promise.all( + [...Object.values(MOCK_GENESIS), `0x${"44".repeat(32)}`].map(supported), + ), + ).toEqual([true, true, true, false]); + expect(mockRuntimeConfig().testStack).toEqual({ network: "mock", - chains: [ - { identifier: "People", genesisHash: MOCK_GENESIS.people }, - { identifier: "Bulletin", genesisHash: MOCK_GENESIS.bulletin }, - { identifier: "AssetHub", genesisHash: MOCK_GENESIS.assetHub }, - ], + tld: "paseo", + people: { genesisHash: MOCK_GENESIS.people }, + bulletin: { genesisHash: MOCK_GENESIS.bulletin }, + assetHub: { genesisHash: MOCK_GENESIS.assetHub }, }); }); @@ -936,9 +944,6 @@ describe("createMockHost TestHostAPI parity", () => { value: { genesisHash: "0x00" }, }), ).rejects.toThrow("unknown"); - await expect(feature.callbacks.features.supportedChains()).rejects.toThrow( - "unknown", - ); const confirmation = createMockHost({ faults: { confirmationError: "no ui" }, diff --git a/js/packages/truapi-host/src/web/create-mock-host.ts b/js/packages/truapi-host/src/web/create-mock-host.ts index dbd01c0a60..0143888242 100644 --- a/js/packages/truapi-host/src/web/create-mock-host.ts +++ b/js/packages/truapi-host/src/web/create-mock-host.ts @@ -43,7 +43,6 @@ import type { import type { AuthState, CoreStorageKey, - HostChainSet, HostScan, JsonRpcConnection, PermissionDecision, @@ -164,7 +163,7 @@ export interface MockFaults { confirmationError?: string; /** Device and remote permission prompts fail with this reason. */ permissionError?: string; - /** `featureSupported` and `supportedChains` fail with this reason. */ + /** `featureSupported` fails with this reason. */ featureError?: string; /** Chat room, bot, and message calls fail with this reason. */ chatError?: string; @@ -406,7 +405,7 @@ export interface MockHostConfig { /** Whether `featureSupported` reports support. Default `true`. */ featureSupported?: boolean; /** - * When set, `featureSupported` answers from `supportedChains`, as a real host + * When set, `featureSupported` answers from `servedChains`, as a real host * does, so a chain outside that set is unsupported. Default `false`. It * becomes the default in a later breaking release. The Rust * `MockConfig::only_served_chains` is the same option. @@ -454,14 +453,12 @@ export interface MockHostConfig { */ chainProxies?: ChainProxy[]; /** - * Chains the host reports serving (RFC 0026). Defaults to the three - * {@link MOCK_GENESIS} chains, which are what {@link mockRuntimeConfig} - * declares. - * - * An empty set type-checks and then fails every chain-routed call, so - * override this only to assert that failure. + * Genesis hashes of the chains the host serves, which `featureSupported` + * answers from when `onlyServedChains` is set. Defaults to the three + * {@link MOCK_GENESIS} chains, the ones {@link mockRuntimeConfig}'s stack + * names. `getChainInfo` answers from that stack, not from this list. */ - supportedChains?: HostChainSet; + servedChains?: string[]; } /** A mock host: the callbacks to wire into a provider, plus assertion oracles. */ @@ -900,14 +897,7 @@ export function createMockHost(config: MockHostConfig = {}): MockHost { languageTag = "en", faults = {}, scanner, - supportedChains = { - network: "mock", - chains: [ - { identifier: "People", genesisHash: MOCK_GENESIS.people }, - { identifier: "Bulletin", genesisHash: MOCK_GENESIS.bulletin }, - { identifier: "AssetHub", genesisHash: MOCK_GENESIS.assetHub }, - ], - }, + servedChains = Object.values(MOCK_GENESIS), } = config; const scan = scanner === undefined ? undefined : { answer: scanner }; @@ -1252,17 +1242,13 @@ export function createMockHost(config: MockHostConfig = {}): MockHost { if (faults.featureError) throw new Error(faults.featureError); const served = !onlyServedChains || - supportedChains.chains.some( - (entry) => - normalizeHash(entry.genesisHash) === + servedChains.some( + (genesisHash) => + normalizeHash(genesisHash) === normalizeHash(request.value.genesisHash), ); return { supported: featureSupported && served }; }, - async supportedChains() { - if (faults.featureError) throw new Error(faults.featureError); - return supportedChains; - }, }, chain: { @@ -1735,13 +1721,63 @@ export const MOCK_GENESIS = { "0x3333333333333333333333333333333333333333333333333333333333333333", } as const; +/** One chain of a {@link TestStack}. */ +export interface TestStackChain { + /** The chain's genesis hash, as 0x-prefixed hex or bytes. */ + genesisHash: string | Uint8Array; +} + +/** + * The stack a test host runs, built from its parts because a test host has no + * signed descriptor to bundle. Only the testing wasm bundle reads it; the + * production bundle takes a {@link ProductRuntimeConfig.stack} only. + */ +export interface TestStack { + /** The network `getChainInfo` reports. */ + network: string; + /** The dotNS TLD a signing host derives its reserved identities under. */ + tld: string; + /** The People chain, which the core runs SSO over. */ + people: TestStackChain; + /** The Bulletin chain, which takes the core's own preimage submissions. */ + bulletin: TestStackChain; + /** Asset Hub, where product manifests resolve from dotNS. */ + assetHub?: TestStackChain; + /** The relay chain. */ + relay?: TestStackChain; + /** + * Whether the stack may use Polkadot's genesis hashes or the `dot` TLD. + * Defaults to `false`, as on a real host. + */ + allowsReservedValues?: boolean; +} + +/** A {@link ProductRuntimeConfig} for a test host, with a {@link TestStack}. */ +export type MockRuntimeConfig = Omit & { + testStack: TestStack; +}; + /** - * A default {@link ProductRuntimeConfig} for a mock host. Override any field; - * the genesis hashes and product id are placeholders suitable for tests. + * The stack a mock host runs by default: network `mock` under the `paseo` + * TLD, over the {@link MOCK_GENESIS} chains. + */ +export function mockTestStack(): TestStack { + return { + network: "mock", + tld: "paseo", + people: { genesisHash: MOCK_GENESIS.people }, + bulletin: { genesisHash: MOCK_GENESIS.bulletin }, + assetHub: { genesisHash: MOCK_GENESIS.assetHub }, + }; +} + +/** + * A default {@link MockRuntimeConfig} for a mock host. Override any field; + * the stack and product id are placeholders suitable for tests. */ export function mockRuntimeConfig( - overrides: Partial = {}, -): ProductRuntimeConfig { + overrides: Partial = {}, +): MockRuntimeConfig { return { productId: asProductId("mock.dot"), host: { @@ -1753,15 +1789,10 @@ export function mockRuntimeConfig( type: "node", version: "0", }, - people: { genesisHash: MOCK_GENESIS.people }, - bulletin: { genesisHash: MOCK_GENESIS.bulletin }, - assetHub: { genesisHash: MOCK_GENESIS.assetHub }, + testStack: mockTestStack(), pairing: { deeplinkScheme: "polkadotapp", }, - // A signing host refuses to start without this; a pairing host ignores it. - // Setting it unconditionally keeps one config usable for both roles. - networkSuffix: "paseo", ...overrides, }; } diff --git a/js/packages/truapi-host/src/web/wasm-bridge.test.ts b/js/packages/truapi-host/src/web/wasm-bridge.test.ts index 35db8134d5..5fd1d898d5 100644 --- a/js/packages/truapi-host/src/web/wasm-bridge.test.ts +++ b/js/packages/truapi-host/src/web/wasm-bridge.test.ts @@ -4,8 +4,18 @@ import { readFileSync } from "node:fs"; import { CoreStorageKey } from "../generated/host-callbacks.js"; import { createMockHost, mockRuntimeConfig } from "./create-mock-host.js"; +import { bundledTestStack } from "../test-support.js"; import { wasmIsBuilt } from "../testing/require-wasm.js"; +/** + * {@link mockRuntimeConfig} with the bundled previewnet stack in place of its + * test stack: the production bundle reads only a signed `stack`. + */ +function bundledRuntimeConfig() { + const { testStack: _testStack, ...config } = mockRuntimeConfig(); + return { ...config, stack: bundledTestStack() }; +} + // Drives the REAL truapi WASM core against createMockHost's callbacks — // headless, no browser, no worker — to prove the JS↔SCALE↔WASM callback bridge. // Requires the built WASM artifact (`npm run build:wasm`); skipped when it is @@ -95,7 +105,7 @@ suite("real WASM core ↔ createMockHost bridge", () => { ...createWasmRawCallbacks(mock.callbacks), requestWorkerTransition: () => {}, }; - const { productId, ...hostConfig } = mockRuntimeConfig(); + const { productId, ...hostConfig } = bundledRuntimeConfig(); const runtime = new WasmPairingHostRuntime(raw, hostConfig); // The core emits response frames through `emitFrame`; the worker supplies // it per-core outside the generated adapter, so the harness does too. @@ -111,6 +121,32 @@ suite("real WASM core ↔ createMockHost bridge", () => { expect(mock.getHostCallCount()).toBeGreaterThan(0); }); + it("refuses a stack pointer without a key", async () => { + // A host refreshes its stack unseen, so the production bundle runs only a + // signed stack, whatever the copy it was handed. + const { initSync, WasmPairingHostRuntime } = await import(glueUrl.href); + const { createWasmRawCallbacks } = + await import("../generated/host-callbacks-adapter.js"); + initSync({ module: readFileSync(wasmUrl) }); + const { productId: _productId, ...hostConfig } = bundledRuntimeConfig(); + const pointer = new URL(hostConfig.stack.pointer); + pointer.searchParams.delete("key"); + + expect( + () => + new WasmPairingHostRuntime( + { + ...createWasmRawCallbacks(createMockHost().callbacks), + requestWorkerTransition: () => {}, + }, + { + ...hostConfig, + stack: { ...hostConfig.stack, pointer: pointer.href }, + }, + ), + ).toThrow("runtimeConfig.stack: stack pointer has no key"); + }); + it("isolates one real-core boot from the next through reset()", async () => { // The surface-agreement test proves the Rust and JS mocks expose the same // control methods. It cannot prove those methods hold against a real core. @@ -122,7 +158,7 @@ suite("real WASM core ↔ createMockHost bridge", () => { initSync({ module: readFileSync(wasmUrl) }); const mock = createMockHost(); - const { productId, ...hostConfig } = mockRuntimeConfig(); + const { productId, ...hostConfig } = bundledRuntimeConfig(); const boot = async () => { const runtime = new WasmPairingHostRuntime( { diff --git a/js/packages/truapi-host/src/web/worker-test-harness.ts b/js/packages/truapi-host/src/web/worker-test-harness.ts index 550910ae07..435e710bf6 100644 --- a/js/packages/truapi-host/src/web/worker-test-harness.ts +++ b/js/packages/truapi-host/src/web/worker-test-harness.ts @@ -14,7 +14,7 @@ import { expect } from "bun:test"; import { asProductId } from "@parity/truapi"; -import { makeHostCallbacks } from "../test-support.js"; +import { bundledTestStack, makeHostCallbacks } from "../test-support.js"; import type { ProductRuntimeConfig } from "../runtime.js"; import { createWebWorkerPairingHostRuntime } from "./index.js"; import type { CreateWebWorkerPairingHostRuntimeOptions } from "./index.js"; @@ -84,18 +84,7 @@ export function runtimeConfig( type: "node", version: process.versions.node, }, - people: { - genesisHash: - "0xa22a2424d2cbf561eaecf7da8b1b548fa9d1939f60265e942b1049616a012f71", - }, - bulletin: { - genesisHash: - "0xbbcccc1cbe333151b8ed63b17e9e0dec61ee53b57296f1fbe2d161ae3e6fb4dc", - }, - assetHub: { - genesisHash: - "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - }, + stack: bundledTestStack(), pairing: { deeplinkScheme: "polkadotapp", }, diff --git a/rust/crates/truapi-codegen/tests/golden/host-callbacks-adapter.ts b/rust/crates/truapi-codegen/tests/golden/host-callbacks-adapter.ts index 63484f6529..03fa23d35b 100644 --- a/rust/crates/truapi-codegen/tests/golden/host-callbacks-adapter.ts +++ b/rust/crates/truapi-codegen/tests/golden/host-callbacks-adapter.ts @@ -34,7 +34,6 @@ import { CoreStorageKey, DevicePermissionStatus, ExtensionVersionMap, - HostChainSet, HostContactLookup, HostContactMatches, HostContactPick, @@ -81,7 +80,6 @@ export interface RawCallbacks { clearCoreStorage(key: Uint8Array): Promise; transactionExtensionVersions(): Promise; featureSupported(request: Uint8Array): Promise; - supportedChains(): Promise; scheduleGameReminder?(product: Uint8Array, startsAt: bigint): Promise; cancelGameReminder?(product: Uint8Array): Promise; subscribeLocale( @@ -211,8 +209,6 @@ export function createWasmRawCallbacks( HostFeatureSupportedRequest.dec(request), ), ), - supportedChains: async () => - HostChainSet.enc(await callbacks.features.supportedChains()), ...(game ? { scheduleGameReminder: async (product, startsAt) => diff --git a/rust/crates/truapi-codegen/tests/golden/host-callbacks.ts b/rust/crates/truapi-codegen/tests/golden/host-callbacks.ts index 27b933015a..a3ff76d331 100644 --- a/rust/crates/truapi-codegen/tests/golden/host-callbacks.ts +++ b/rust/crates/truapi-codegen/tests/golden/host-callbacks.ts @@ -9,7 +9,6 @@ import * as S from "@parity/truapi/scale"; import { AllocatableResource, Bytes32, - ChainIdentifier, CodeFormat, FundingDirection, FundingQuote, @@ -421,37 +420,6 @@ export type FundingQuoteUnavailable = */ | { tag: "Timeout"; value?: undefined }; -/** - * One chain a host serves: a protocol chain role mapped to the concrete - * chain of the host's configured environment. - */ -export interface HostChainEntry { - /** - * Protocol role this entry answers for. - */ - identifier: ChainIdentifier; - - /** - * Genesis hash identifying the chain in all chain-scoped calls. - */ - genesisHash: Bytes32; -} - -/** - * The chain set a host serves: its environment plus one entry per chain role. - */ -export interface HostChainSet { - /** - * Ecosystem the host is configured for, e.g. "polkadot", "paseo". - */ - network: string; - - /** - * Chains this host serves, keyed by protocol role. - */ - chains: Array; -} - /** * Contact handles the core needs turned back into accounts, and the key they * were minted under. @@ -1188,29 +1156,6 @@ export const FundingQuoteUnavailable: S.Codec = S.lazy( }), ); -/** - * One chain a host serves: a protocol chain role mapped to the concrete - * chain of the host's configured environment. - */ -export const HostChainEntry: S.Codec = S.lazy( - (): S.Codec => - S.Struct({ - identifier: ChainIdentifier, - genesisHash: Bytes32, - }) as S.Codec, -); - -/** - * The chain set a host serves: its environment plus one entry per chain role. - */ -export const HostChainSet: S.Codec = S.lazy( - (): S.Codec => - S.Struct({ - network: S.str, - chains: S.Vector(HostChainEntry), - }) as S.Codec, -); - /** * Contact handles the core needs turned back into accounts, and the key they * were minted under. @@ -1874,12 +1819,6 @@ export interface Features { featureSupported( request: HostFeatureSupportedRequest, ): Promise; - - /** - * Enumerate the chains this host serves (RFC 0026). The core resolves - * `get_chain_info` requests against the returned set. - */ - supportedChains(): Promise; } /** diff --git a/rust/crates/truapi-codegen/tests/golden/wasm_bridge.rs b/rust/crates/truapi-codegen/tests/golden/wasm_bridge.rs index 1e94ce0b48..04a995fc04 100644 --- a/rust/crates/truapi-codegen/tests/golden/wasm_bridge.rs +++ b/rust/crates/truapi-codegen/tests/golden/wasm_bridge.rs @@ -40,7 +40,6 @@ pub struct JsBridge { pub clear_core_storage: Function, pub transaction_extension_versions: Function, pub feature_supported: Function, - pub supported_chains: Function, pub schedule_game_reminder: Function, pub cancel_game_reminder: Function, pub subscribe_locale: Function, @@ -98,7 +97,6 @@ impl JsBridge { "transactionExtensionVersions", )?, feature_supported: get_function(callbacks, "featureSupported")?, - supported_chains: get_function(callbacks, "supportedChains")?, schedule_game_reminder: get_optional_function(callbacks, "scheduleGameReminder")? .unwrap_or_else(|| missing_callback("scheduleGameReminder")), cancel_game_reminder: get_optional_function(callbacks, "cancelGameReminder")? @@ -379,16 +377,6 @@ impl crate::platform::Features for WasmPlatform { "featureSupported response did not decode", )?) } - - async fn supported_chains( - &self, - ) -> Result { - let bytes = invoke_bytes_return(&self.bridge.supported_chains, Vec::new()).await?; - Ok(decode_bytes::( - bytes, - "supportedChains response did not decode", - )?) - } } #[async_trait::async_trait] diff --git a/rust/crates/truapi-codegen/tests/golden/worker-callbacks.ts b/rust/crates/truapi-codegen/tests/golden/worker-callbacks.ts index 70ae5f668e..3997263783 100644 --- a/rust/crates/truapi-codegen/tests/golden/worker-callbacks.ts +++ b/rust/crates/truapi-codegen/tests/golden/worker-callbacks.ts @@ -23,7 +23,6 @@ export const CALLBACK_NAMES = [ "clearCoreStorage", "transactionExtensionVersions", "featureSupported", - "supportedChains", "scheduleGameReminder", "cancelGameReminder", "navigateTo", @@ -79,7 +78,6 @@ function rawCallbacks( | "clearCoreStorage" | "transactionExtensionVersions" | "featureSupported" - | "supportedChains" | "navigateTo" | "pushNotification" | "cancelNotification" @@ -117,10 +115,6 @@ function rawCallbacks( bridge.callbackRequest("featureSupported", [request]) as ReturnType< Required["featureSupported"] >, - supportedChains: () => - bridge.callbackRequest("supportedChains", []) as ReturnType< - Required["supportedChains"] - >, navigateTo: (url) => bridge.callbackRequest("navigateTo", [url]) as ReturnType< Required["navigateTo"] diff --git a/rust/crates/truapi-host-cli/README.md b/rust/crates/truapi-host-cli/README.md index b57e660cb0..5148633a50 100644 --- a/rust/crates/truapi-host-cli/README.md +++ b/rust/crates/truapi-host-cli/README.md @@ -1112,7 +1112,7 @@ role), its `dub` service as the identity backend (the `dub` URL with `/api/v1` appended), its `tld` and its `dotns.popController` replace the network's. A stack that lacks one of those roles, gives two of them one genesis hash, has a `tld` the core does not serve (`paseo`, `testnet`), is unreachable or is past -`expiresAt` is an error. The product namespace (`HostChainSet.network` and the +`expiresAt` is an error. The product namespace (the network `get_chain_info` reports and the state directory) is the descriptor's `id` without a trailing `-fork`. Use a separate `--base-path` and a disposable identity for a local stack. The routing applies to every chain role the host serves, including the Bulletin node the CLI diff --git a/rust/crates/truapi-host-cli/SPEC.md b/rust/crates/truapi-host-cli/SPEC.md index b91ce6e047..c0b1853003 100644 --- a/rust/crates/truapi-host-cli/SPEC.md +++ b/rust/crates/truapi-host-cli/SPEC.md @@ -1757,7 +1757,7 @@ descriptor that breaks a rule and one past `expiresAt` are errors that name the stack; one past `validUntil` is used with a warning. Its endpoints and People's `NetworkSuffix` are checked as above. `--chain-backend light-client` is refused with `--stack`, since the light client syncs only the networks -`truapi-provider` bundles. The product namespace (`HostChainSet.network` and the +`truapi-provider` bundles. The product namespace (the network `get_chain_info` reports and the state directory) is the descriptor's `id` without a trailing `-fork`, which must be a lowercase label. diff --git a/rust/crates/truapi-host-cli/src/chain/tests.rs b/rust/crates/truapi-host-cli/src/chain/tests.rs index 12764b6f1c..545e0849b2 100644 --- a/rust/crates/truapi-host-cli/src/chain/tests.rs +++ b/rust/crates/truapi-host-cli/src/chain/tests.rs @@ -182,22 +182,22 @@ fn every_served_role_routes_to_its_own_chain() { for network in Network::value_variants() { let config = network.config(); let provider = CliChainProvider::with_live_chain_routing(&config, false); - for entry in config.host_chain_set().chains { - let expected = config.url_for_role(entry.identifier).unwrap_or_else(|| { - panic!("{} serves {:?} with no preset URL", config.id, entry.identifier) + for (identifier, genesis_hash) in config.served_roles() { + let expected = config.url_for_role(identifier).unwrap_or_else(|| { + panic!("{} serves {:?} with no preset URL", config.id, identifier) }); assert!( - provider.routes(&entry.genesis_hash), + provider.routes(&genesis_hash), "{} serves {:?} but does not route it; the fallback would hide this", config.id, - entry.identifier + identifier ); assert_eq!( - provider.location(entry.genesis_hash), + provider.location(genesis_hash), expected, "{} serves {:?} but routes it elsewhere", config.id, - entry.identifier + identifier ); } } diff --git a/rust/crates/truapi-host-cli/src/frame_server/tests.rs b/rust/crates/truapi-host-cli/src/frame_server/tests.rs index f9b61692b1..bb364f9e8a 100644 --- a/rust/crates/truapi-host-cli/src/frame_server/tests.rs +++ b/rust/crates/truapi-host-cli/src/frame_server/tests.rs @@ -81,10 +81,7 @@ fn signing_runtime() -> Result> { platform: truapi::latest::HostPlatform::Cli, }, truapi::platform::PlatformInfo { kind: Some("test".into()), version: None }, - network.people_genesis, - network.bulletin_genesis, - network.asset_hub_genesis, - network.network_suffix.to_string(), + network.stack()?, )?; let spawner: truapi::subscription::Spawner = Arc::new(|_| {}); Ok(Arc::new(SigningHostRuntime::new(platform, config, spawner))) @@ -489,10 +486,7 @@ mod cross_product_storage { platform: truapi::latest::HostPlatform::Cli, }, truapi::platform::PlatformInfo { kind: Some("test".into()), version: None }, - network.people_genesis, - network.bulletin_genesis, - network.asset_hub_genesis, - network.network_suffix.to_string(), + network.stack()?, )?; // An override written to a field this path never reads compiles, // changes nothing, and looks identical in timings. Assert it. diff --git a/rust/crates/truapi-host-cli/src/main.rs b/rust/crates/truapi-host-cli/src/main.rs index 19f4a4efbb..fb9f3ca3c5 100644 --- a/rust/crates/truapi-host-cli/src/main.rs +++ b/rust/crates/truapi-host-cli/src/main.rs @@ -1348,9 +1348,7 @@ async fn run_pairing_host( let config = PairingHostConfig::new( host_info("Headless Pairing Host"), platform_info(), - network.people_genesis, - network.bulletin_genesis, - network.asset_hub_genesis, + network.stack().context("invalid stack")?, DEEPLINK_SCHEME.to_string(), ) .context("invalid pairing host config")?; @@ -1895,10 +1893,7 @@ fn build_signing_runtime( let config = SigningHostConfig::new( host_info("Headless Signing Host"), platform_info(), - network.people_genesis, - network.bulletin_genesis, - network.asset_hub_genesis, - network.network_suffix.to_string(), + network.stack().context("invalid stack")?, ) .context("invalid signing host config")?; let status_host = platform.clone() as Arc; diff --git a/rust/crates/truapi-host-cli/src/network.rs b/rust/crates/truapi-host-cli/src/network.rs index 4078540f74..3b61ad2ce3 100644 --- a/rust/crates/truapi-host-cli/src/network.rs +++ b/rust/crates/truapi-host-cli/src/network.rs @@ -6,7 +6,7 @@ use trinity_stack::{Descriptor, Fetched, Pointer, Status, accept}; use truapi::{ latest::ChainIdentifier, platform::{ - ChainExtensionVersion, DOTNS_TLDS, ExtensionVersionMap, HostChainEntry, HostChainSet, + ChainExtensionVersion, DOTNS_TLDS, ExtensionVersionMap, Stack, StackChains, StackError, }, }; @@ -35,7 +35,7 @@ pub enum Network { PaseoNextV2, /// Built-in stack, read from the descriptor its operator publishes. Its id is /// the id the playground and dotli already use for this network - /// (`VITE_NETWORKS=paseo-next-v2,previewnet`), so `HostChainSet::network` + /// (`VITE_NETWORKS=paseo-next-v2,previewnet`), so the network `get_chain_info` /// reads the same string a product sees everywhere else. #[value(name = "previewnet")] Previewnet, @@ -917,7 +917,7 @@ const POLKADOT: NetworkConfig = NetworkConfig { /// Resolved RPC/backend/genesis values for one network. #[derive(Debug, Clone, Copy)] pub struct NetworkConfig { - /// The product namespace: what `HostChainSet::network` reports and where + /// The product namespace: the network `get_chain_info` reports and where /// the CLI stores this network's state. pub id: &'static str, /// The network's dotNS TLD without the dot, as its runtimes report it in @@ -937,7 +937,7 @@ pub struct NetworkConfig { pub people_genesis: [u8; 32], pub bulletin_genesis: [u8; 32], /// Asset Hub genesis hash, both the role's identity in - /// [`NetworkConfig::host_chain_set`] and its routing key, and the genesis the + /// [`NetworkConfig::served_roles`] and its routing key, and the genesis the /// CLI's Asset Hub extrinsics sign in `CheckGenesis`. /// /// Wrong here is worse than stale elsewhere: routing matches on it, so a value @@ -990,10 +990,10 @@ impl NetworkConfig { Ok(()) } - /// The chain set this network serves, for `Features::supported_chains`. + /// The chain roles this network serves, each with its genesis hash. /// /// One entry per role this struct names a genesis hash for. `ChainEndpoint` - /// carries no role, so `live_chain_endpoints` cannot supply one — an endpoint's + /// carries no role, so `live_chain_endpoints` cannot supply one: an endpoint's /// identifier is not recoverable from it, which is why a served role needs its /// own field here rather than a lookup into that list. /// @@ -1001,24 +1001,32 @@ impl NetworkConfig { /// unmapped genesis with People, so a role the routing filter drops would /// connect to the wrong chain while the host claimed to serve it. /// `chain::tests::every_served_role_routes_to_its_own_chain` holds that. - pub fn host_chain_set(&self) -> HostChainSet { - HostChainSet { - network: self.id.to_string(), - chains: vec![ - HostChainEntry { - identifier: ChainIdentifier::People, - genesis_hash: self.people_genesis, - }, - HostChainEntry { - identifier: ChainIdentifier::Bulletin, - genesis_hash: self.bulletin_genesis, - }, - HostChainEntry { - identifier: ChainIdentifier::AssetHub, - genesis_hash: self.asset_hub_genesis, - }, - ], - } + pub fn served_roles(&self) -> [(ChainIdentifier, [u8; 32]); 3] { + [ + (ChainIdentifier::People, self.people_genesis), + (ChainIdentifier::Bulletin, self.bulletin_genesis), + (ChainIdentifier::AssetHub, self.asset_hub_genesis), + ] + } + + /// The stack the core runs on: [`Self::served_roles`], the network `id` + /// `get_chain_info` reports, and the TLD. The CLI resolved and checked these + /// values itself, from a descriptor, the local-network file or Polkadot's + /// pinned values, so it hands them to the core as parts. Only Polkadot may + /// use Polkadot's reserved values. + pub fn stack(&self) -> Result { + let chains = StackChains { + relay: None, + asset_hub: Some(self.asset_hub_genesis), + people: self.people_genesis, + bulletin: self.bulletin_genesis, + }; + Stack::new( + self.id.to_string(), + self.network_suffix.to_string(), + chains, + self.id == POLKADOT.id, + ) } /// The chains this network builds as V5 general transactions: People and diff --git a/rust/crates/truapi-host-cli/src/network/local_network/tests.rs b/rust/crates/truapi-host-cli/src/network/local_network/tests.rs index 8f9df0ee03..d6e53994b2 100644 --- a/rust/crates/truapi-host-cli/src/network/local_network/tests.rs +++ b/rust/crates/truapi-host-cli/src/network/local_network/tests.rs @@ -20,13 +20,13 @@ fn test_local_routes_presets() { assert_eq!(resolved.network_suffix, base.network_suffix); assert_eq!(resolved.identity_backend_base, Some("http://127.0.0.1:8092/api/v1")); assert_eq!(resolved.live_chain_endpoints.len(), 3); - for role in resolved.host_chain_set().chains { + for (identifier, genesis_hash) in resolved.served_roles() { let route = resolved .live_chain_endpoints .iter() - .find(|route| route.genesis == role.genesis_hash) + .find(|route| route.genesis == genesis_hash) .unwrap(); - assert_eq!(Some(route.ws), resolved.url_for_role(role.identifier)); + assert_eq!(Some(route.ws), resolved.url_for_role(identifier)); assert!(route.required_for_host); assert!(route.ws.starts_with("ws://127.0.0.1:")); } diff --git a/rust/crates/truapi-host-cli/src/network/tests.rs b/rust/crates/truapi-host-cli/src/network/tests.rs index 6f4c811f13..07d4419284 100644 --- a/rust/crates/truapi-host-cli/src/network/tests.rs +++ b/rust/crates/truapi-host-cli/src/network/tests.rs @@ -13,20 +13,20 @@ use super::*; fn served_chain_genesis_hashes_match_the_endpoint_routes() { for network in Network::value_variants() { let config = network.config(); - for entry in config.host_chain_set().chains { - let expected_ws = config.url_for_role(entry.identifier).unwrap_or_else(|| { - panic!("{} serves {:?} with no URL", config.id, entry.identifier) - }); + for (identifier, genesis_hash) in config.served_roles() { + let expected_ws = config + .url_for_role(identifier) + .unwrap_or_else(|| panic!("{} serves {:?} with no URL", config.id, identifier)); let endpoint = config .live_chain_endpoints .iter() - .find(|endpoint| endpoint.genesis == entry.genesis_hash) + .find(|endpoint| endpoint.genesis == genesis_hash) .unwrap_or_else(|| { panic!( "{} serves {:?} at genesis {} which no endpoint routes", config.id, - entry.identifier, - hex::encode(entry.genesis_hash) + identifier, + hex::encode(genesis_hash) ) }); assert_eq!(endpoint.ws, expected_ws); @@ -42,7 +42,7 @@ fn test_network_served_roles() { for network in Network::value_variants() { let config = network.config(); let served: Vec = - config.host_chain_set().chains.iter().map(|entry| entry.identifier).collect(); + config.served_roles().iter().map(|(identifier, _)| *identifier).collect(); assert_eq!( served, @@ -153,8 +153,8 @@ async fn the_advertised_genesis_matches_what_each_chain_reports() { for network in Network::value_variants() { let config = network.config(); - for entry in config.host_chain_set().chains { - let ws = config.url_for_role(entry.identifier).expect("every served role has a URL"); + for (identifier, genesis_hash) in config.served_roles() { + let ws = config.url_for_role(identifier).expect("every served role has a URL"); let probe = async { let rpc = alloc::rpc::RpcClient::connect(ws) .await @@ -171,7 +171,7 @@ async fn the_advertised_genesis_matches_what_each_chain_reports() { let (reported, chain_name) = match probe.await { Ok(values) => values, Err(err) => { - unreachable.push(format!("{:?} at {ws}: {err}", entry.identifier)); + unreachable.push(format!("{:?} at {ws}: {err}", identifier)); continue; }, }; @@ -179,7 +179,7 @@ async fn the_advertised_genesis_matches_what_each_chain_reports() { checked += 1; // Paseo's People chain calls itself "People" and previewnet's calls // itself "Individuality Local", so either name satisfies the role. - let expected_in_name: &[&str] = match entry.identifier { + let expected_in_name: &[&str] = match identifier { ChainIdentifier::People => &["People", "Individuality"], ChainIdentifier::Bulletin => &["Bulletin"], ChainIdentifier::AssetHub => &["Asset Hub"], @@ -189,21 +189,21 @@ async fn the_advertised_genesis_matches_what_each_chain_reports() { drifted.push(format!( "{} serves {:?} from {ws}, which calls itself {chain_name:?} and \ contains none of {expected_in_name:?}", - config.id, entry.identifier + config.id, identifier )); - } else if entry.genesis_hash == reported { + } else if genesis_hash == reported { println!( "{} {:?} {} matches {ws} ({chain_name})", config.id, - entry.identifier, - hex::encode(entry.genesis_hash) + identifier, + hex::encode(genesis_hash) ); } else { drifted.push(format!( "{} advertises {:?} as {} but {ws} reports {}", config.id, - entry.identifier, - hex::encode(entry.genesis_hash), + identifier, + hex::encode(genesis_hash), hex::encode(reported) )); } @@ -227,7 +227,7 @@ async fn the_advertised_genesis_matches_what_each_chain_reports() { assert!(failures.is_empty(), "{}", failures.join("\n\n")); let expected_checks: usize = Network::value_variants() .iter() - .map(|network| network.config().host_chain_set().chains.len()) + .map(|network| network.config().served_roles().len()) .sum(); assert!(expected_checks > 0); assert_eq!(checked, expected_checks); @@ -960,3 +960,37 @@ fn test_refresh_expired_error() { let published = expired(11); assert!(refresh(&sign_copy(10, "g1"), Fetched::Document(&published)).is_err()); } + +/// The core answers `get_chain_info` from the stack the CLI builds, so the +/// stack carries the network id products see and each served role's genesis. +#[test] +fn the_stack_names_the_configured_network() { + let config = Network::default().config(); + let stack = config.stack().expect("the default network is a valid stack"); + + assert_eq!( + (stack.network(), stack.tld(), stack.chains().clone()), + ( + config.id, + config.network_suffix, + truapi::platform::StackChains { + relay: None, + asset_hub: Some(config.asset_hub_genesis), + people: config.people_genesis, + bulletin: config.bulletin_genesis, + } + ), + ); +} + +/// Polkadot runs on Polkadot's reserved genesis hashes and `dot`, which the +/// core refuses on any stack its host does not vouch for. Every built-in +/// network has to start, Polkadot included. +#[test] +fn every_built_in_network_builds_a_stack() { + let refused: Vec<_> = Network::value_variants() + .iter() + .filter_map(|network| network.config().stack().err().map(|error| (*network, error))) + .collect(); + assert_eq!(refused, Vec::new()); +} diff --git a/rust/crates/truapi-host-cli/src/platform.rs b/rust/crates/truapi-host-cli/src/platform.rs index 6972a510df..91c91c4936 100644 --- a/rust/crates/truapi-host-cli/src/platform.rs +++ b/rust/crates/truapi-host-cli/src/platform.rs @@ -167,8 +167,9 @@ impl CliStoragePaths { /// Headless-host platform shared by both roles. pub struct CliPlatform { chain: Arc, - /// Chain roles this host serves, answered by `Features::supported_chains`. - chains: truapi::platform::HostChainSet, + /// Genesis hashes of the chain roles this host serves, which + /// `Features::feature_supported` answers from. + served_chains: Vec<[u8; 32]>, extension_versions: truapi::platform::ExtensionVersionMap, product_storage: Mutex>>>, core_storage: Mutex, Vec>>, @@ -256,7 +257,7 @@ impl CliPlatform { let bitswap = BitswapRpc::new(Arc::clone(&chain), network.bulletin_genesis); Arc::new(Self { chain, - chains: network.host_chain_set(), + served_chains: network.served_roles().map(|(_, genesis)| genesis).to_vec(), extension_versions: network.extension_versions(), product_storage: Mutex::new(product_storage), core_storage: Mutex::new(core_storage), @@ -806,16 +807,11 @@ impl Features for CliPlatform { ) -> Result { let api::HostFeatureSupportedRequest::Chain { genesis_hash } = request; let supported = self - .chains - .chains + .served_chains .iter() - .any(|entry| entry.genesis_hash.as_slice() == genesis_hash.as_slice()); + .any(|served| served.as_slice() == genesis_hash.as_slice()); Ok(api::HostFeatureSupportedResponse { supported }) } - - async fn supported_chains(&self) -> Result { - Ok(self.chains.clone()) - } } impl truapi::platform::AuthPresenter for CliPlatform { diff --git a/rust/crates/truapi-host-cli/src/platform/tests.rs b/rust/crates/truapi-host-cli/src/platform/tests.rs index f4d1fec97b..52317eeb9e 100644 --- a/rust/crates/truapi-host-cli/src/platform/tests.rs +++ b/rust/crates/truapi-host-cli/src/platform/tests.rs @@ -13,9 +13,9 @@ fn test_network() -> crate::network::NetworkConfig { /// registration and both create-transaction variants, and `AssetHub` for the /// other seventeen. /// -/// `feature_supported` answers from the same set `supported_chains` serves, so -/// the two cannot disagree. The negatives are the malformed inputs, a well-formed -/// hash the host serves no role for, and the all-zero SSO sentinel — which the +/// `feature_supported` answers from the roles the network serves, the same +/// ones its stack names, so the two cannot disagree. The negatives are the malformed inputs, a +/// well-formed hash the host serves no role for, and the all-zero SSO sentinel — which the /// provider does route, to the People fallback, yet is still not a served role. #[test] fn feature_supported_resolves_against_the_served_chain_set() { @@ -33,11 +33,11 @@ fn feature_supported_resolves_against_the_served_chain_set() { // Every role the host serves answers supported. The reverse direction is the // `unserved` assertion below, since every served role is now a real chain. - for entry in config.host_chain_set().chains { + for (identifier, genesis_hash) in config.served_roles() { assert!( - supported(entry.genesis_hash.to_vec()), + supported(genesis_hash.to_vec()), "{:?} is served but reported unsupported", - entry.identifier + identifier ); } @@ -46,11 +46,7 @@ fn feature_supported_resolves_against_the_served_chain_set() { // bytes" and only the all-zero sentinel would notice. let unserved = [0xab; 32]; assert!( - !config - .host_chain_set() - .chains - .iter() - .any(|entry| entry.genesis_hash == unserved), + !config.served_roles().iter().any(|(_, genesis_hash)| *genesis_hash == unserved), "the stand-in must not be a served role" ); assert!(!supported(unserved.to_vec())); @@ -62,28 +58,6 @@ fn feature_supported_resolves_against_the_served_chain_set() { assert!(!supported(vec![0u8; 32])); } -#[test] -fn supported_chains_answers_the_configured_network() { - let platform = CliPlatform::new(test_network(), None, ApprovalPolicy::AutoAccept, None); - let set = futures::executor::block_on(platform.supported_chains()) - .expect("the CLI host serves the preset's chains"); - - let config = crate::network::Network::default().config(); - assert_eq!(set.network, config.id); - let mut served = set - .chains - .iter() - .map(|entry| (entry.identifier, hex::encode(entry.genesis_hash))) - .collect::>(); - served.sort_by_key(|(identifier, _)| format!("{identifier:?}")); - let mut expected = vec![ - (api::ChainIdentifier::People, hex::encode(config.people_genesis)), - (api::ChainIdentifier::Bulletin, hex::encode(config.bulletin_genesis)), - (api::ChainIdentifier::AssetHub, hex::encode(config.asset_hub_genesis)), - ]; - expected.sort_by_key(|(identifier, _)| format!("{identifier:?}")); - assert_eq!(served, expected); -} use tempfile::tempdir; #[test] diff --git a/rust/crates/truapi/Cargo.toml b/rust/crates/truapi/Cargo.toml index 2fbc7229a7..956aae7278 100644 --- a/rust/crates/truapi/Cargo.toml +++ b/rust/crates/truapi/Cargo.toml @@ -37,6 +37,7 @@ runtime = [ # only with codec's own serde support. "parity-scale-codec/serde", "dep:truapi-provider", + "dep:trinity-stack", "dep:futures-timer", "dep:scale-decode", "dep:frame-metadata", @@ -105,6 +106,7 @@ hex = { workspace = true } parity-scale-codec = { workspace = true, features = ["derive"] } truapi-provider = { path = "../truapi-provider", default-features = false, optional = true } +trinity-stack = { path = "../trinity-stack", optional = true } futures-timer = { workspace = true, optional = true } scale-decode = { workspace = true, features = ["derive"], optional = true } # Metadata decoding backs allowance registration, which both the native and the diff --git a/rust/crates/truapi/RUNTIME.md b/rust/crates/truapi/RUNTIME.md index 2f8fcf460e..af1bf5a777 100644 --- a/rust/crates/truapi/RUNTIME.md +++ b/rust/crates/truapi/RUNTIME.md @@ -213,14 +213,26 @@ a compile error, not a runtime `Unavailable`. A local session's usernames come from the dotNS contracts on Asset Hub, read after activation. A name the host passes is shown until a record answers. -`SigningHostConfig.network_suffix` is the network's bare dotNS TLD (`dot`, -`paseo`, or `testnet`). The shell supplies it alongside the chain genesis hashes -from the same network configuration used by wallet onboarding. It must match -the People chain's `NetworkSuffix.NetworkSuffix`: reserved identities derive -under `uid.` and `peopl.`, while the chain uses that suffix for -proof contexts. Configuration keeps local activation and key derivation -available offline. The core validates supported suffixes but does not -automatically check that the configured suffix matches the chain. +### Stack + +Both host configs carry a `Stack`, the core's only source of chain +configuration: the People, Bulletin, Asset Hub and relay genesis hashes, the +network `get_chain_info` reports, and the dotNS TLD. A native or browser shell +passes the stack's pointer (`polkadotapp://stack?key=0x…&url=…`), the signed +descriptor its build bundles, and whether the build allows reserved values; +`Stack::bundled` verifies the copy against the pointer's key. It refuses a +pointer without a key, a descriptor without a People or Bulletin chain, a TLD +the core does not serve dotNS under, and Polkadot's genesis hashes or the `dot` +TLD on a stack the build does not allow them on. A Rust host that resolved a +stack itself, such as the CLI, builds one with `Stack::new` under the same +rules, and a test-host browser bundle takes one built from its parts as +`runtimeConfig.testStack`. + +The TLD must match the People chain's `NetworkSuffix.NetworkSuffix`: reserved +identities derive under `uid.` and `peopl.`, while the chain uses that +suffix for proof contexts. Reading it from the bundled copy keeps local +activation and key derivation available offline. The core does not yet check +the descriptor against the live chains, and does not fetch newer copies. ### Core database diff --git a/rust/crates/truapi/src/host_core.rs b/rust/crates/truapi/src/host_core.rs index 6f21881914..c036268d1a 100644 --- a/rust/crates/truapi/src/host_core.rs +++ b/rust/crates/truapi/src/host_core.rs @@ -239,9 +239,7 @@ impl PairingHostRuntime { let services = RuntimeServices::with_chat_platform( platform, config.host.host_info.clone(), - config.people_chain_genesis_hash, - config.bulletin_chain_genesis_hash, - config.asset_hub_chain_genesis_hash, + config.stack.clone(), spawner.clone(), chat_platform, ); @@ -724,9 +722,7 @@ impl SigningHostRuntime { let services = RuntimeServices::with_chat_platform( platform, config.host.host_info.clone(), - config.people_chain_genesis_hash, - config.bulletin_chain_genesis_hash, - config.asset_hub_chain_genesis_hash, + config.stack.clone(), spawner, chat_platform, ); @@ -738,11 +734,11 @@ impl SigningHostRuntime { // indistinguishable from an ungranted read. Only as visible as the // host's log level, which defaults to `ERROR`. warn!( - "no Asset Hub configured: no product manifest will resolve, so \ + "the stack has no Asset Hub: no product manifest will resolve, so \ every cross-product grant not already cached is refused" ); } - let signing_host = SigningHostRole::new(services.clone(), config.network_suffix); + let signing_host = SigningHostRole::new(services.clone()); let sso_responder = SsoResponderService::new(services.clone(), signing_host.clone()); Self { image_loader: crate::image_loader::ImageLoader::new( diff --git a/rust/crates/truapi/src/host_core/tests.rs b/rust/crates/truapi/src/host_core/tests.rs index adb91624bf..e290571adc 100644 --- a/rust/crates/truapi/src/host_core/tests.rs +++ b/rust/crates/truapi/src/host_core/tests.rs @@ -44,11 +44,7 @@ fn activated_signing_runtime(platform: Arc) -> SigningHostRuntime platform: truapi::latest::HostPlatform::Unknown, }, PlatformInfo::default(), - [0; 32], - [0xbb; 32], - // Distinct from its siblings so a transposition stays visible. - [0xcc; 32], - "testnet".to_string(), + crate::test_support::test_stack("testnet", [0; 32], [0xbb; 32], Some([0xcc; 32])), ) .expect("signing host config is valid"); let runtime = SigningHostRuntime::new(platform, config, test_spawner()); @@ -1571,10 +1567,7 @@ fn the_device_pairing_observer_is_installed_once() { platform: truapi::latest::HostPlatform::Unknown, }, PlatformInfo::default(), - [0; 32], - [0xbb; 32], - [0xcc; 32], - "paseo".to_string(), + crate::test_support::test_stack("paseo", [0; 32], [0xbb; 32], Some([0xcc; 32])), ) .expect("signing host config is valid"); let runtime = @@ -1599,10 +1592,7 @@ fn test_signing_runtime() -> (SigningHostRuntime, Arc) { let config = SigningHostConfig::new( host, PlatformInfo::default(), - genesis.0, - genesis.1, - genesis.2, - "paseo".to_string(), + crate::test_support::test_stack("paseo", genesis.0, genesis.1, Some(genesis.2)), ) .expect("signing host config is valid"); let platform = Arc::new(StubPlatform::default()); @@ -1724,10 +1714,7 @@ fn answer_sso_request_distinguishes_disconnect_from_ignorable_messages() { platform: truapi::latest::HostPlatform::Unknown, }, PlatformInfo::default(), - [0; 32], - [0xbb; 32], - [0xcc; 32], - "paseo".to_string(), + crate::test_support::test_stack("paseo", [0; 32], [0xbb; 32], Some([0xcc; 32])), ) .expect("signing host config is valid"); let runtime = @@ -1771,10 +1758,7 @@ fn answer_sso_request_returns_a_correlated_response() { platform: truapi::latest::HostPlatform::Unknown, }, PlatformInfo::default(), - [0; 32], - [0xbb; 32], - [0xcc; 32], - "paseo".to_string(), + crate::test_support::test_stack("paseo", [0; 32], [0xbb; 32], Some([0xcc; 32])), ) .expect("signing host config is valid"); let runtime = @@ -1891,7 +1875,9 @@ fn disconnect_paired_host_propagates_submission_failure() { /// Signing-host config carrying `asset_hub`, otherwise the shape every /// other signing test here uses. -fn signing_config_with_asset_hub(asset_hub: [u8; 32]) -> crate::platform::SigningHostConfig { +fn signing_config_with_asset_hub( + asset_hub: Option<[u8; 32]>, +) -> crate::platform::SigningHostConfig { use crate::platform::{HostInfo, PlatformInfo, SigningHostConfig}; SigningHostConfig::new( @@ -1902,12 +1888,7 @@ fn signing_config_with_asset_hub(asset_hub: [u8; 32]) -> crate::platform::Signin platform: truapi::latest::HostPlatform::Unknown, }, PlatformInfo::default(), - // Same-typed and positional, so a transposition only shows up if - // no two are equal. - [0xaa; 32], - [0xbb; 32], - asset_hub, - "paseo".to_string(), + crate::test_support::test_stack("paseo", [0xaa; 32], [0xbb; 32], asset_hub), ) .expect("signing host config is valid") } @@ -1919,7 +1900,7 @@ fn a_signing_host_runtime_carries_its_asset_hub_for_manifest_resolution() { // consulted, which is why a seeded CLI looked healthy. let runtime = SigningHostRuntime::new( Arc::new(StubPlatform::default()), - signing_config_with_asset_hub([0xcc; 32]), + signing_config_with_asset_hub(Some([0xcc; 32])), test_spawner(), ); assert_eq!( @@ -1943,9 +1924,7 @@ fn a_pairing_host_runtime_carries_its_asset_hub_too() { platform: truapi::latest::HostPlatform::Web, }, PlatformInfo::default(), - [0; 32], - [0xbb; 32], - [0xdd; 32], + crate::test_support::test_stack("paseo", [0; 32], [0xbb; 32], Some([0xdd; 32])), "polkadotapp".to_string(), ) .expect("pairing host config is valid"); @@ -1964,7 +1943,7 @@ fn an_all_zero_asset_hub_is_how_a_signing_host_says_it_has_none() { // second sentinel crossing the boundary. let runtime = SigningHostRuntime::new( Arc::new(StubPlatform::default()), - signing_config_with_asset_hub([0; 32]), + signing_config_with_asset_hub(None), test_spawner(), ); // The hash is a constructor argument, so `None` here can only mean the @@ -1985,9 +1964,7 @@ fn the_asset_hub_argument_reaches_the_asset_hub_slot() { version: None, platform: truapi::latest::HostPlatform::Unknown, }, - [0xaa; 32], - [0xbb; 32], - [0xcc; 32], + crate::test_support::test_stack("paseo", [0xaa; 32], [0xbb; 32], Some([0xcc; 32])), test_spawner(), ); assert_eq!( @@ -2007,7 +1984,7 @@ struct ManifestLookup { /// A cross-product storage read for `owner`, uncached, on a signing-role /// product runtime configured with `asset_hub`. -fn signing_manifest_lookup_rpc(asset_hub: [u8; 32]) -> ManifestLookup { +fn signing_manifest_lookup_rpc(asset_hub: Option<[u8; 32]>) -> ManifestLookup { use truapi::{api::LocalStorage, versioned::local_storage::HostLocalStorageReadRequest}; // The stub serves no dotNS either way, so end the follow rather than @@ -2046,7 +2023,7 @@ fn signing_manifest_lookup_rpc(asset_hub: [u8; 32]) -> ManifestLookup { fn a_signing_host_takes_a_manifest_miss_to_the_chain() { // The refusal is identical either way, so whether the core asked the // chain is the only observable difference. - let configured = signing_manifest_lookup_rpc([0xcc; 32]); + let configured = signing_manifest_lookup_rpc(Some([0xcc; 32])); assert!( !configured.rpc.is_empty(), "a configured signing role resolves an uncached manifest over dotNS" @@ -2061,7 +2038,7 @@ fn a_signing_host_takes_a_manifest_miss_to_the_chain() { Bulletin ([0xbb; 32])" ); - let unconfigured = signing_manifest_lookup_rpc([0; 32]); + let unconfigured = signing_manifest_lookup_rpc(None); assert!( unconfigured.rpc.is_empty(), "a signing role with no Asset Hub refuses without touching the chain" diff --git a/rust/crates/truapi/src/host_logic/features.rs b/rust/crates/truapi/src/host_logic/features.rs index d38a9b5754..8d319cfb86 100644 --- a/rust/crates/truapi/src/host_logic/features.rs +++ b/rust/crates/truapi/src/host_logic/features.rs @@ -1,18 +1,12 @@ //! Feature-detection delegation. //! -//! `feature_supported` and `supported_chains` are platform syscalls: each -//! host owns the set of chains it can service. This module is a thin shim -//! that forwards through to [`crate::platform::Features`], plus the in-core -//! RFC-0026 resolution that answers `get_chain_info` from the host's chain -//! set so per-request semantics (ordering, `NotSupported`) stay core-owned. +//! `feature_supported` is a platform syscall: each host owns the set of chains +//! it can service. This module is a thin shim that forwards through to +//! [`crate::platform::Features`]. `get_chain_info` is answered by the core +//! from its [`crate::platform::Stack`] instead. -use crate::platform::{Features, HostChainSet}; -use truapi::{ - latest::{ - ChainIdentifier, RemoteChainInfoError, RemoteChainInfoRequest, RemoteChainInfoResponse, - }, - v01::{GenericError, HostFeatureSupportedRequest, HostFeatureSupportedResponse}, -}; +use crate::platform::Features; +use truapi::v01::{GenericError, HostFeatureSupportedRequest, HostFeatureSupportedResponse}; /// Forward a feature-support query to the platform implementation. pub async fn feature_supported( @@ -22,78 +16,5 @@ pub async fn feature_supported( platform.feature_supported(request).await } -/// Fetch the host's chain set from the platform implementation. -pub async fn supported_chains( - platform: &P, -) -> Result { - platform.supported_chains().await -} - -/// The genesis hash the host serves for `chain`, or `None` when it serves no -/// such chain. -/// -/// Runtime code that needs to talk to a chain by role resolves it here rather -/// than carrying a configured hash, so a chain the host does not serve is a -/// missing entry instead of a connection to the wrong chain. -pub fn genesis_for(set: &HostChainSet, chain: ChainIdentifier) -> Option<[u8; 32]> { - set.chains - .iter() - .find(|entry| entry.identifier == chain) - .map(|entry| entry.genesis_hash) -} - -/// Resolve a `get_chain_info` request against the host's chain set: the -/// requested identifier's genesis hash plus the host's network, echoing the -/// identifier, or `NotSupported` when the host does not serve it. -pub fn chain_info( - set: &HostChainSet, - request: &RemoteChainInfoRequest, -) -> Result { - genesis_for(set, request.chain) - .map(|genesis_hash| RemoteChainInfoResponse { - network: set.network.clone(), - chain: request.chain, - genesis_hash, - }) - .ok_or(RemoteChainInfoError::NotSupported) -} - #[cfg(test)] mod tests; - -#[cfg(test)] -mod genesis_lookup_tests { - use super::*; - use crate::platform::HostChainEntry; - - fn set() -> HostChainSet { - HostChainSet { - network: "paseo".to_string(), - chains: vec![HostChainEntry { - identifier: ChainIdentifier::AssetHub, - genesis_hash: [0xab; 32], - }], - } - } - - /// A chain the host does not serve is absent, not a different chain's hash. - #[test] - fn only_served_chains_resolve() { - assert_eq!(genesis_for(&set(), ChainIdentifier::AssetHub), Some([0xab; 32])); - assert_eq!(genesis_for(&set(), ChainIdentifier::Bulletin), None); - } - - /// `chain_info` answers from the same lookup, so the product-facing method and - /// in-core callers cannot disagree about what the host serves. - #[test] - fn chain_info_agrees_with_the_lookup() { - let set = set(); - for chain in [ChainIdentifier::AssetHub, ChainIdentifier::People] { - let request = RemoteChainInfoRequest { chain }; - assert_eq!( - chain_info(&set, &request).ok().map(|info| info.genesis_hash), - genesis_for(&set, chain), - ); - } - } -} diff --git a/rust/crates/truapi/src/host_logic/features/tests.rs b/rust/crates/truapi/src/host_logic/features/tests.rs index f9b9c68a63..8d907ab56c 100644 --- a/rust/crates/truapi/src/host_logic/features/tests.rs +++ b/rust/crates/truapi/src/host_logic/features/tests.rs @@ -1,16 +1,4 @@ use super::*; -use crate::platform::HostChainEntry; -use truapi::latest::ChainIdentifier; - -fn paseo_set() -> HostChainSet { - HostChainSet { - network: "paseo".to_string(), - chains: vec![ - HostChainEntry { identifier: ChainIdentifier::AssetHub, genesis_hash: [0xaa; 32] }, - HostChainEntry { identifier: ChainIdentifier::People, genesis_hash: [0xbb; 32] }, - ], - } -} struct AlwaysSupported; @@ -23,10 +11,6 @@ impl Features for AlwaysSupported { assert!(matches!(request, HostFeatureSupportedRequest::Chain { .. })); Ok(HostFeatureSupportedResponse { supported: true }) } - - async fn supported_chains(&self) -> Result { - Ok(paseo_set()) - } } struct AlwaysUnsupported; @@ -40,10 +24,6 @@ impl Features for AlwaysUnsupported { assert!(matches!(request, HostFeatureSupportedRequest::Chain { .. })); Ok(HostFeatureSupportedResponse { supported: false }) } - - async fn supported_chains(&self) -> Result { - Err(GenericError { reason: "no chains".to_string() }) - } } fn req() -> HostFeatureSupportedRequest { @@ -61,31 +41,3 @@ fn delegates_unsupported_to_platform() { let resp = futures::executor::block_on(feature_supported(&AlwaysUnsupported, req())).unwrap(); assert!(!resp.supported); } - -#[test] -fn delegates_supported_chains_to_platform() { - let set = futures::executor::block_on(supported_chains(&AlwaysSupported)).unwrap(); - assert_eq!(set, paseo_set()); -} - -#[test] -fn surfaces_supported_chains_platform_error() { - let err = futures::executor::block_on(supported_chains(&AlwaysUnsupported)).unwrap_err(); - assert_eq!(err.reason, "no chains"); -} - -#[test] -fn resolves_identifier_and_echoes_it() { - let request = RemoteChainInfoRequest { chain: ChainIdentifier::People }; - let response = chain_info(&paseo_set(), &request).unwrap(); - assert_eq!(response.network, "paseo"); - assert_eq!(response.chain, ChainIdentifier::People); - assert_eq!(response.genesis_hash, [0xbb; 32]); -} - -#[test] -fn unserved_identifier_is_not_supported() { - let request = RemoteChainInfoRequest { chain: ChainIdentifier::Bulletin }; - let err = chain_info(&paseo_set(), &request).unwrap_err(); - assert_eq!(err, RemoteChainInfoError::NotSupported); -} diff --git a/rust/crates/truapi/src/host_logic/product_account.rs b/rust/crates/truapi/src/host_logic/product_account.rs index 1f8a535c7f..3254d12f22 100644 --- a/rust/crates/truapi/src/host_logic/product_account.rs +++ b/rust/crates/truapi/src/host_logic/product_account.rs @@ -6,7 +6,7 @@ //! Reserved built-ins additionally pin the `uid.` identity account and //! the `peopl.` full/lite ring-VRF keyed-hash paths used by pairing //! attestation, where `` is the network's dotNS TLD (`dot`, `paseo`, -//! `testnet`) from [`crate::platform::SigningHostConfig::network_suffix`]. The +//! `testnet`) from [`crate::platform::Stack::tld`]. The //! People chain scopes its proof contexts with the same suffix, so one //! network has one person per seed. RFC-0024 operational key selection comes //! from the registry. diff --git a/rust/crates/truapi/src/host_logic/sso/pairing/tests.rs b/rust/crates/truapi/src/host_logic/sso/pairing/tests.rs index 62c6c7c540..c5041dec7a 100644 --- a/rust/crates/truapi/src/host_logic/sso/pairing/tests.rs +++ b/rust/crates/truapi/src/host_logic/sso/pairing/tests.rs @@ -23,9 +23,7 @@ fn runtime_config() -> PairingHostConfig { platform: truapi::latest::HostPlatform::Web, }, PlatformInfo { kind: Some("Firefox".to_string()), version: Some("192.32".to_string()) }, - [0; 32], - [0xbb; 32], - [0xcc; 32], + crate::test_support::test_stack("paseo", [0; 32], [0xbb; 32], Some([0xcc; 32])), "polkadotapp".to_string(), ) .expect("test runtime config is valid") diff --git a/rust/crates/truapi/src/image_loader/tests.rs b/rust/crates/truapi/src/image_loader/tests.rs index 6971cbdfa5..7c0abc18f5 100644 --- a/rust/crates/truapi/src/image_loader/tests.rs +++ b/rust/crates/truapi/src/image_loader/tests.rs @@ -40,9 +40,7 @@ fn image_loader(platform: Arc, timer: Arc) -> ImageLo version: None, platform: truapi::latest::HostPlatform::Unknown, }, - [1; 32], - BULLETIN_GENESIS, - [3; 32], + crate::test_support::test_stack("paseo", [1; 32], BULLETIN_GENESIS, Some([3; 32])), test_spawner(), ); ImageLoader::new(services, timer) diff --git a/rust/crates/truapi/src/lib.rs b/rust/crates/truapi/src/lib.rs index e25661378d..ad14f4a67b 100644 --- a/rust/crates/truapi/src/lib.rs +++ b/rust/crates/truapi/src/lib.rs @@ -871,6 +871,8 @@ mod runtime; #[cfg(feature = "runtime")] mod session_usernames; #[cfg(feature = "runtime")] +mod stack; +#[cfg(feature = "runtime")] mod stream_extensions; #[cfg(feature = "runtime")] pub mod subscription; diff --git a/rust/crates/truapi/src/markers.rs b/rust/crates/truapi/src/markers.rs index 3c856c2ccf..0a56e76d36 100644 --- a/rust/crates/truapi/src/markers.rs +++ b/rust/crates/truapi/src/markers.rs @@ -143,9 +143,6 @@ markers! { /// The host was asked whether it supports a feature. Detail names it. NATIVE_FEATURE_SUPPORTED = "truapi.native.callback.feature_supported" => Local; - /// The host was asked which chains it supports. - NATIVE_SUPPORTED_CHAINS = "truapi.native.callback.supported_chains" => Local; - /// The host was asked for a chain's transaction extension versions. NATIVE_TRANSACTION_EXTENSION_VERSIONS = "truapi.native.callback.transaction_extension_versions" => Local; diff --git a/rust/crates/truapi/src/native.rs b/rust/crates/truapi/src/native.rs index 3773f4e0cc..173e323ec1 100644 --- a/rust/crates/truapi/src/native.rs +++ b/rust/crates/truapi/src/native.rs @@ -42,7 +42,9 @@ pub use callbacks::{ NativeFundingCallbacks, NativeGameCallbacks, NativePaymentCallbacks, NativeScannerCallbacks, NativeTopUpCallbacks, }; -pub use config::{HostRuntimeConfig, NativeRuntimeConfigError, ProductExecutionConfig}; +pub use config::{ + BundledStack, HostRuntimeConfig, NativeRuntimeConfigError, ProductExecutionConfig, +}; pub use errors::{ HostRejection, NativeChatFieldError, NativeCoreDatabaseError, NativeRendererError, }; diff --git a/rust/crates/truapi/src/native/callbacks.rs b/rust/crates/truapi/src/native/callbacks.rs index 798b24c036..b0fa7472ea 100644 --- a/rust/crates/truapi/src/native/callbacks.rs +++ b/rust/crates/truapi/src/native/callbacks.rs @@ -153,11 +153,6 @@ pub trait HostCallbacks: Send + Sync { request: latest::HostFeatureSupportedRequest, ) -> Result; - /// Enumerate the chains this host serves (RFC 0026): its environment plus - /// one entry per chain role. Invoked on the dispatcher thread; must return - /// promptly. - fn supported_chains(&self) -> Result; - /// The chains the host builds as V5 general transactions, each with its /// transaction extension pipeline. A chain not listed is built as V4. async fn transaction_extension_versions( diff --git a/rust/crates/truapi/src/native/config.rs b/rust/crates/truapi/src/native/config.rs index dcad20f825..562ef0f760 100644 --- a/rust/crates/truapi/src/native/config.rs +++ b/rust/crates/truapi/src/native/config.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use crate::{ platform::{ ExecutableKind, HostInfo, PlatformInfo, ProductContext, ProductId, - RuntimeConfigValidationError, SigningHostConfig, + RuntimeConfigValidationError, SigningHostConfig, Stack, }, pocket_cards::PinnedPocketCard, }; @@ -26,25 +26,10 @@ pub struct HostRuntimeConfig { /// Optional platform/browser version shown by the wallet during SSO pairing. #[uniffi(default)] pub platform_version: Option, - /// People-chain genesis hash. Must be exactly 32 bytes. - pub people_chain_genesis_hash: Vec, - /// Bulletin-chain genesis hash. Must be exactly 32 bytes. - pub bulletin_chain_genesis_hash: Vec, - /// Asset Hub genesis hash, where the dotNS contracts are deployed. Must be - /// exactly 32 bytes. - /// - /// Product manifests are read from dotNS, so this is what makes a - /// `trustedProducts` grant resolvable. 32 zero bytes says this host has no - /// Asset Hub; grants already in the manifest cache stay honoured until they - /// expire. - pub asset_hub_chain_genesis_hash: Vec, - /// The network's dotNS TLD without the leading dot (`dot`, `paseo`, - /// `testnet`). The wallet's reserved identities are derived under it: - /// `uid.` for the identity account, `peopl.` for the person - /// ring-VRF keys. Read it from the network the host is configured for, the - /// way the host's own onboarding does; a wrong value derives a different - /// person from the same seed. - pub network_suffix: String, + /// The stack the host build runs, which every chain the core uses, the + /// network `get_chain_info` reports, and the TLD the wallet's reserved + /// identities derive under all come from. + pub stack: BundledStack, /// Existing, writable directory for core-owned databases, kept out of /// device backups. The runtime opens its database there at startup. pub database_directory: String, @@ -61,6 +46,21 @@ pub struct HostRuntimeConfig { pub local_session_lite_username: Option, } +/// The stack a host build ships: its pointer, the descriptor copy the build +/// verified, and whether the build lets it use Polkadot's reserved values. +#[derive(Clone, uniffi::Record)] +pub struct BundledStack { + /// The stack's link, `polkadotapp://stack?key=0x…&url=…`. It must carry a + /// key. + pub pointer: String, + /// The bundled descriptor's bytes, as served: a signed envelope that + /// verifies under the pointer's key. + pub descriptor: Vec, + /// Whether this build may run a stack that uses Polkadot's genesis hashes + /// or the `dot` TLD. Only a Polkadot or Polkadot-fork build sets it. + pub allows_reserved_values: bool, +} + /// Trusted identity attached by a native host to one executable connection. #[derive(Clone, uniffi::Record)] pub struct ProductExecutionConfig { @@ -120,13 +120,6 @@ impl From for NativeRuntimeConfigError { } } -/// A 32-byte genesis hash handed over as bytes, or the error naming `field`. -fn genesis_hash(field: &str, bytes: &[u8]) -> Result<[u8; 32], NativeRuntimeConfigError> { - bytes.try_into().map_err(|_| NativeRuntimeConfigError::Invalid { - reason: format!("{field} must be exactly 32 bytes, got {}", bytes.len()), - }) -} - /// The platform products see in `System::host_info`. The native library ships /// only inside iOS and Android apps, so the target decides it. pub fn native_host_platform() -> HostPlatform { @@ -143,12 +136,9 @@ impl TryFrom for NativeResolvedHostRuntimeConfig { type Error = NativeRuntimeConfigError; fn try_from(config: HostRuntimeConfig) -> Result { - let people_chain_genesis_hash = - genesis_hash("people_chain_genesis_hash", &config.people_chain_genesis_hash)?; - let bulletin_chain_genesis_hash = - genesis_hash("bulletin_chain_genesis_hash", &config.bulletin_chain_genesis_hash)?; - let asset_hub_chain_genesis_hash = - genesis_hash("asset_hub_chain_genesis_hash", &config.asset_hub_chain_genesis_hash)?; + let BundledStack { pointer, descriptor, allows_reserved_values } = config.stack; + let stack = Stack::bundled(&pointer, &descriptor, allows_reserved_values) + .map_err(|error| NativeRuntimeConfigError::Invalid { reason: error.to_string() })?; let signing = SigningHostConfig::new( HostInfo { name: config.host_name, @@ -157,10 +147,7 @@ impl TryFrom for NativeResolvedHostRuntimeConfig { platform: native_host_platform(), }, PlatformInfo { kind: config.platform_type, version: config.platform_version }, - people_chain_genesis_hash, - bulletin_chain_genesis_hash, - asset_hub_chain_genesis_hash, - config.network_suffix, + stack, )?; Ok(Self { signing, diff --git a/rust/crates/truapi/src/native/platform.rs b/rust/crates/truapi/src/native/platform.rs index 26c85e664f..3a5c779d58 100644 --- a/rust/crates/truapi/src/native/platform.rs +++ b/rust/crates/truapi/src/native/platform.rs @@ -268,13 +268,6 @@ impl Features for CallbackPlatform { let supported = self.callbacks.feature_supported(request).await?; Ok(v01::HostFeatureSupportedResponse { supported }) } - - async fn supported_chains(&self) -> Result { - self.callbacks - .on_core_log(markers::NATIVE_SUPPORTED_CHAINS.target.to_string(), String::new()); - - self.callbacks.supported_chains().map_err(v01::GenericError::from) - } } #[async_trait::async_trait] diff --git a/rust/crates/truapi/src/native/tests.rs b/rust/crates/truapi/src/native/tests.rs index 031835abd8..a1338431a2 100644 --- a/rust/crates/truapi/src/native/tests.rs +++ b/rust/crates/truapi/src/native/tests.rs @@ -342,9 +342,6 @@ impl HostCallbacks for EventCallbacks { ) -> Result { Ok(false) } - fn supported_chains(&self) -> Result { - Ok(crate::platform::HostChainSet { network: "paseo".to_string(), chains: Vec::new() }) - } async fn transaction_extension_versions( &self, @@ -505,10 +502,12 @@ pub fn native_host_runtime_config() -> HostRuntimeConfig { host_version: None, platform_type: None, platform_version: None, - people_chain_genesis_hash: vec![0xa2; 32], - bulletin_chain_genesis_hash: vec![0xbb; 32], - asset_hub_chain_genesis_hash: vec![0xcc; 32], - network_suffix: "paseo".to_string(), + stack: native_bundled_stack(&crate::test_support::test_descriptor( + "paseo", + [0xa2; 32], + [0xbb; 32], + Some([0xcc; 32]), + )), // Every runtime opens its own database; the directory is left for the // OS to clean so the fixture can stay a plain value. database_directory: tempfile::tempdir().unwrap().keep().to_string_lossy().into_owned(), @@ -518,6 +517,12 @@ pub fn native_host_runtime_config() -> HostRuntimeConfig { } } +/// `descriptor` signed, as a native host bundles it. +fn native_bundled_stack(descriptor: &serde_json::Value) -> crate::native::BundledStack { + let (pointer, descriptor) = crate::test_support::sign_test_descriptor(descriptor); + crate::native::BundledStack { pointer, descriptor, allows_reserved_values: false } +} + pub fn native_execution_config( product_id: &str, execution_kind: ExecutableKind, @@ -1774,80 +1779,76 @@ fn runtime_config_reports_the_platform_the_library_was_built_for() { } #[test] -fn runtime_config_rejects_wrong_size_genesis_hash() { - let err = NativeResolvedHostRuntimeConfig::try_from(HostRuntimeConfig { - people_chain_genesis_hash: vec![0; 31], - ..native_host_runtime_config() - }) - .unwrap_err(); - - assert_eq!( - err, - NativeRuntimeConfigError::Invalid { - reason: "people_chain_genesis_hash must be exactly 32 bytes, got 31".to_string(), - } - ); -} - -#[test] -fn each_configured_genesis_hash_reaches_its_own_field() { - // Three adjacent `Vec` feeding a positional constructor: - // transposing any two compiles and, without this, passes. +fn each_descriptor_chain_reaches_its_own_role() { let resolved = NativeResolvedHostRuntimeConfig::try_from(HostRuntimeConfig { - people_chain_genesis_hash: vec![0xa1; 32], - bulletin_chain_genesis_hash: vec![0xb2; 32], - asset_hub_chain_genesis_hash: vec![0xc3; 32], + stack: native_bundled_stack(&crate::test_support::test_descriptor( + "paseo", + [0xa1; 32], + [0xb2; 32], + Some([0xc3; 32]), + )), ..native_host_runtime_config() }) .expect("config is valid"); assert_eq!( - ( - resolved.signing.people_chain_genesis_hash, - resolved.signing.bulletin_chain_genesis_hash, - resolved.signing.asset_hub_chain_genesis_hash, - ), - ([0xa1; 32], [0xb2; 32], [0xc3; 32]), + resolved.signing.stack.chains(), + &crate::platform::StackChains { + relay: Some([0x01; 32]), + asset_hub: Some([0xc3; 32]), + people: [0xa1; 32], + bulletin: [0xb2; 32], + }, ); } #[test] -fn a_wrong_size_asset_hub_genesis_hash_is_rejected_as_its_own_field() { - // An empty vec must be an error, never a silent all-zero "no Asset - // Hub". - for len in [0usize, 31, 33] { - let err = NativeResolvedHostRuntimeConfig::try_from(HostRuntimeConfig { - asset_hub_chain_genesis_hash: vec![0; len], - ..native_host_runtime_config() - }) - .unwrap_err(); - - assert_eq!( - err, - NativeRuntimeConfigError::Invalid { - reason: format!("asset_hub_chain_genesis_hash must be exactly 32 bytes, got {len}"), - } - ); - } -} - -#[test] -fn runtime_config_rejects_a_network_suffix_that_is_not_a_bare_tld() { - // The suffix ends every reserved derivation (`peopl.`), so a - // shell passing the dotted form would silently derive a stranger. +fn runtime_config_refuses_a_stack_pointer_without_a_key() { + // The host refreshes its stack unseen, so only a signed stack may start. + let mut stack = native_host_runtime_config().stack; + stack.pointer = "polkadotapp://stack?url=https%3A%2F%2Fstack.test.invalid%2Fstack.json".into(); let err = NativeResolvedHostRuntimeConfig::try_from(HostRuntimeConfig { - network_suffix: ".paseo".to_string(), + stack, ..native_host_runtime_config() }) .unwrap_err(); + assert_eq!( err, NativeRuntimeConfigError::Invalid { - reason: r#"network_suffix must be a supported dotNS TLD, got ".paseo""#.to_string(), + reason: crate::platform::StackError::Unsigned.to_string() } ); } +#[test] +fn runtime_config_passes_the_reserved_values_flag_through() { + // A testnet build must not run a stack on `dot`; a Polkadot build may. + let mut stack = native_bundled_stack(&crate::test_support::test_descriptor( + "dot", [0xa1; 32], [0xb2; 32], None, + )); + let refused = NativeResolvedHostRuntimeConfig::try_from(HostRuntimeConfig { + stack: stack.clone(), + ..native_host_runtime_config() + }); + stack.allows_reserved_values = true; + let allowed = NativeResolvedHostRuntimeConfig::try_from(HostRuntimeConfig { + stack, + ..native_host_runtime_config() + }); + + assert_eq!( + (refused.err(), allowed.map(|resolved| resolved.signing.stack.tld().to_owned()).ok()), + ( + Some(NativeRuntimeConfigError::Invalid { + reason: "stack uses Polkadot's reserved tld dot, which this build does not allow" + .to_string(), + }), + Some("dot".to_string()), + ), + ); +} + /// The app passes a product id as a string, and UniFFI parses it while /// converting the call's arguments, so an execution can only open under a /// valid id. @@ -2021,9 +2022,6 @@ fn start_ws_bridge_twice_returns_already_running() { ) -> Result { Ok(false) } - fn supported_chains(&self) -> Result { - Ok(crate::platform::HostChainSet { network: "paseo".to_string(), chains: Vec::new() }) - } async fn transaction_extension_versions( &self, @@ -2213,9 +2211,6 @@ fn pending_permission_decision_does_not_stall_bridge() { ) -> Result { Ok(true) } - fn supported_chains(&self) -> Result { - Ok(crate::platform::HostChainSet { network: "paseo".to_string(), chains: Vec::new() }) - } async fn transaction_extension_versions( &self, diff --git a/rust/crates/truapi/src/native/ws_bridge/tests.rs b/rust/crates/truapi/src/native/ws_bridge/tests.rs index c666dd48a4..4b0e0ffe09 100644 --- a/rust/crates/truapi/src/native/ws_bridge/tests.rs +++ b/rust/crates/truapi/src/native/ws_bridge/tests.rs @@ -26,10 +26,7 @@ fn runtime_factory_for(platform: Arc) -> Arc pub host: HostIdentity, - /// People-chain genesis hash used for statement-store SSO. - pub people_chain_genesis_hash: [u8; 32], - /// Bulletin-chain genesis hash used for in-core preimage submission. - pub bulletin_chain_genesis_hash: [u8; 32], - /// Asset Hub genesis hash. Session usernames and product manifests are - /// both read from the dotNS contracts deployed there, so without a usable - /// value no manifest resolves and every cross-product `trustedProducts` - /// grant not already cached is refused, indistinguishably from the other - /// product having granted nothing. All-zero says this host has no Asset - /// Hub. - pub asset_hub_chain_genesis_hash: [u8; 32], + /// The stack the host runs: People for statement-store SSO, Bulletin for + /// in-core preimage submission, and Asset Hub, whose dotNS contracts hold + /// session usernames and product manifests. Without an Asset Hub no + /// manifest resolves, and every cross-product `trustedProducts` grant not + /// already cached is refused. + pub stack: Stack, /// Deeplink URI scheme used in pairing QR payloads, without `://`. /// /// Host-spec L.2-L.3 define the `polkadotapp://pair` route and construction @@ -106,22 +101,11 @@ pub struct SigningHostConfig { /// parity with [`PairingHostConfig`] and for the future signer-side SSO /// responder, which advertises host identity in handshake responses. pub host: HostIdentity, - /// People-chain genesis hash used for statement-store product calls. - pub people_chain_genesis_hash: [u8; 32], - /// Bulletin-chain genesis hash used for in-core preimage submission. - pub bulletin_chain_genesis_hash: [u8; 32], - /// Asset Hub genesis hash the dotNS contracts are deployed on, used to - /// resolve the product manifests that carry `trustedProducts` grants. - /// - /// All-zero says this host has no Asset Hub, which refuses every grant not - /// already in the manifest cache. - pub asset_hub_chain_genesis_hash: [u8; 32], - /// The network's dotNS TLD without the leading dot: `dot`, `paseo`, - /// `testnet`. Every reserved RFC-0022 identity the wallet derives ends in - /// it: the `uid.` identity account and the `peopl.` person - /// ring-VRF keys. Must match the People chain's - /// `NetworkSuffix.NetworkSuffix` value used for proof contexts. - pub network_suffix: String, + /// The stack the host runs. Its TLD must match the People chain's + /// `NetworkSuffix.NetworkSuffix`: every reserved RFC-0022 identity the + /// wallet derives ends in it, so a wrong one derives a different person + /// from the same seed. + pub stack: Stack, } /// Product identity attached to one product-facing TrUAPI connection. @@ -222,9 +206,7 @@ impl PairingHostConfig { pub fn new( host_info: HostInfo, platform_info: PlatformInfo, - people_chain_genesis_hash: [u8; 32], - bulletin_chain_genesis_hash: [u8; 32], - asset_hub_chain_genesis_hash: [u8; 32], + stack: Stack, pairing_deeplink_scheme: String, ) -> Result { require_non_empty("pairing_deeplink_scheme", &pairing_deeplink_scheme)?; @@ -235,9 +217,7 @@ impl PairingHostConfig { } let config = Self { host: HostIdentity::new(host_info, platform_info)?, - people_chain_genesis_hash, - bulletin_chain_genesis_hash, - asset_hub_chain_genesis_hash, + stack, pairing_deeplink_scheme, }; Ok(config) @@ -250,32 +230,12 @@ impl SigningHostConfig { pub fn new( host_info: HostInfo, platform_info: PlatformInfo, - people_chain_genesis_hash: [u8; 32], - bulletin_chain_genesis_hash: [u8; 32], - asset_hub_chain_genesis_hash: [u8; 32], - network_suffix: String, + stack: Stack, ) -> Result { - validate_network_suffix(&network_suffix)?; - Ok(Self { - host: HostIdentity::new(host_info, platform_info)?, - people_chain_genesis_hash, - bulletin_chain_genesis_hash, - asset_hub_chain_genesis_hash, - network_suffix, - }) + Ok(Self { host: HostIdentity::new(host_info, platform_info)?, stack }) } } -fn validate_network_suffix(network_suffix: &str) -> Result<(), RuntimeConfigValidationError> { - require_non_empty("network_suffix", network_suffix)?; - if !DOTNS_TLDS.contains(&network_suffix) { - return Err(RuntimeConfigValidationError::InvalidNetworkSuffix { - network_suffix: network_suffix.to_string(), - }); - } - Ok(()) -} - impl ProductContext { /// A product context for an app. pub fn new(product_id: ProductId) -> Self { @@ -778,12 +738,6 @@ pub enum RuntimeConfigValidationError { #[display("{_0}")] #[from] InvalidProductId(#[error(not(source))] ProductIdError), - /// Network suffix was not a supported dotNS TLD. - #[display("network_suffix must be a supported dotNS TLD, got {network_suffix:?}")] - InvalidNetworkSuffix { - /// Actual network suffix value. - network_suffix: String, - }, } const PRODUCT_STORAGE_KEY_PREFIX: &str = "truapi:product-storage:v1:"; @@ -1130,27 +1084,6 @@ pub trait PairingHostAdmin: Send + Sync { fn notify_session_store_changed(&self); } -/// One chain a host serves: a protocol chain role mapped to the concrete -/// chain of the host's configured environment. -#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] -#[cfg_attr(not(target_arch = "wasm32"), derive(uniffi::Record))] -pub struct HostChainEntry { - /// Protocol role this entry answers for. - pub identifier: ChainIdentifier, - /// Genesis hash identifying the chain in all chain-scoped calls. - pub genesis_hash: Bytes32, -} - -/// The chain set a host serves: its environment plus one entry per chain role. -#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] -#[cfg_attr(not(target_arch = "wasm32"), derive(uniffi::Record))] -pub struct HostChainSet { - /// Ecosystem the host is configured for, e.g. "polkadot", "paseo". - pub network: String, - /// Chains this host serves, keyed by protocol role. - pub chains: Vec, -} - /// Feature-support probing. The host answers whether it can service a given /// capability (currently scoped to per-chain support). #[async_trait::async_trait] @@ -1160,10 +1093,6 @@ pub trait Features: Send + Sync { &self, request: HostFeatureSupportedRequest, ) -> Result; - - /// Enumerate the chains this host serves (RFC 0026). The core resolves - /// `get_chain_info` requests against the returned set. - async fn supported_chains(&self) -> Result; } /// A chain the host builds as V5 general transactions, and the transaction diff --git a/rust/crates/truapi/src/platform/mock.rs b/rust/crates/truapi/src/platform/mock.rs index 896a29d6c7..59363872ca 100644 --- a/rust/crates/truapi/src/platform/mock.rs +++ b/rust/crates/truapi/src/platform/mock.rs @@ -214,7 +214,7 @@ pub struct MockFaults { pub confirmation_error: Option, /// Device and remote permission prompts fail with this reason. pub permission_error: Option, - /// `feature_supported` and `supported_chains` fail with this reason. + /// `feature_supported` fails with this reason. pub feature_error: Option, /// Chat room, bot, and message calls fail with this reason. pub chat_error: Option, @@ -229,7 +229,7 @@ pub struct MockConfig { pub remote_permissions: PermissionPolicy, /// Whether `feature_supported` reports support. pub feature_supported: bool, - /// When set, `feature_supported` answers from `supported_chains`, as a real + /// When set, `feature_supported` answers from `served_chains`, as a real /// host does, so a chain outside that set is unsupported. It becomes the /// default in a later breaking release. The JS mock's `onlyServedChains` is /// the same option. @@ -241,13 +241,12 @@ pub struct MockConfig { /// Whether `confirm_user_action` confirms reviewed actions. /// [`MockPlatform::set_confirm_user_actions`] changes it for one test. pub confirm_user_actions: bool, - /// Chains the mock reports serving (RFC 0026). + /// Genesis hashes of the chains the mock serves RPC for. /// - /// Defaults to the three [`mock_genesis`] chains, the same set the - /// TypeScript mock declares, so a suite written against either one is - /// answered the same way. Replace it to exercise a host serving fewer - /// chains, declaring each with the genesis hash its runtime config carries. - pub supported_chains: crate::platform::HostChainSet, + /// Defaults to the three [`mock_genesis`] chains, the ones + /// [`mock_genesis::stack`] names and the TypeScript mock serves, so a suite + /// written against either one is answered the same way. + pub served_chains: Vec, /// Chain connection behavior. pub chain: ChainBehavior, /// Error injection. @@ -264,7 +263,11 @@ impl Default for MockConfig { theme: latest::ThemeVariant::Dark, language_tag: "en".to_string(), confirm_user_actions: true, - supported_chains: mock_genesis::chain_set(), + served_chains: vec![ + mock_genesis::PEOPLE, + mock_genesis::BULLETIN, + mock_genesis::ASSET_HUB, + ], chain: ChainBehavior::Silent, faults: MockFaults::default(), } @@ -981,6 +984,18 @@ impl Notifications for MockPlatform { } } +/// A stack for tests that need particular genesis hashes: network `mock` +/// under the `paseo` TLD, without a relay. +pub fn mock_stack( + people: truapi::Bytes32, + bulletin: truapi::Bytes32, + asset_hub: Option, +) -> crate::platform::Stack { + let chains = crate::platform::StackChains { relay: None, asset_hub, people, bulletin }; + crate::platform::Stack::new("mock".to_string(), "paseo".to_string(), chains, false) + .expect("a mock stack names no reserved value") +} + /// Placeholder genesis hashes for the chains a mock host serves. /// /// The TypeScript mock exports the same three under `MOCK_GENESIS`, byte for @@ -994,26 +1009,10 @@ pub mod mock_genesis { /// Genesis hash the mock reports for the asset hub chain. pub const ASSET_HUB: truapi::Bytes32 = [0x33; 32]; - /// The chain set a mock host serves by default. - pub fn chain_set() -> crate::platform::HostChainSet { - use truapi::latest::ChainIdentifier; - crate::platform::HostChainSet { - network: "mock".to_string(), - chains: vec![ - crate::platform::HostChainEntry { - identifier: ChainIdentifier::People, - genesis_hash: PEOPLE, - }, - crate::platform::HostChainEntry { - identifier: ChainIdentifier::Bulletin, - genesis_hash: BULLETIN, - }, - crate::platform::HostChainEntry { - identifier: ChainIdentifier::AssetHub, - genesis_hash: ASSET_HUB, - }, - ], - } + /// The stack a mock host runs: [`super::mock_stack`] over the three mock + /// chains. + pub fn stack() -> crate::platform::Stack { + super::mock_stack(PEOPLE, BULLETIN, Some(ASSET_HUB)) } } @@ -1064,15 +1063,6 @@ impl crate::platform::ExtrinsicVersionHost for MockPlatform {} #[async_trait::async_trait] impl Features for MockPlatform { - async fn supported_chains( - &self, - ) -> Result { - if let Some(reason) = &self.config.faults.feature_error { - return Err(latest::GenericError { reason: reason.clone() }); - } - Ok(self.config.supported_chains.clone()) - } - async fn feature_supported( &self, request: latest::HostFeatureSupportedRequest, @@ -1083,10 +1073,9 @@ impl Features for MockPlatform { let latest::HostFeatureSupportedRequest::Chain { genesis_hash } = request; let served = !self.config.only_served_chains || self.config - .supported_chains - .chains + .served_chains .iter() - .any(|entry| entry.genesis_hash.as_slice() == genesis_hash.as_slice()); + .any(|served| served.as_slice() == genesis_hash.as_slice()); Ok(latest::HostFeatureSupportedResponse { supported: self.config.feature_supported && served, }) diff --git a/rust/crates/truapi/src/platform/mock/tests.rs b/rust/crates/truapi/src/platform/mock/tests.rs index 7d1f1ad15f..1ddeb60a54 100644 --- a/rust/crates/truapi/src/platform/mock/tests.rs +++ b/rust/crates/truapi/src/platform/mock/tests.rs @@ -302,7 +302,7 @@ fn feature_supported_reports_unserved_chains_only_when_asked() { assert!(supported(&MockPlatform::new(), unserved.clone())); let strict = MockPlatform::with_config(MockConfig { only_served_chains: true, ..Default::default() }); - let served = strict.config.supported_chains.chains[0].genesis_hash.to_vec(); + let served = strict.config.served_chains[0].to_vec(); assert!(supported(&strict, served)); assert!(!supported(&strict, unserved)); } @@ -910,47 +910,30 @@ fn injected_permission_and_feature_faults_surface_as_errors() { )) .is_err() ); - assert!(block_on(p.supported_chains()).is_err()); // A failed prompt is not a recorded decision. assert!(p.permission_log().is_empty()); } #[test] -fn declared_supported_chains_are_what_the_mock_reports() { - // The default set is spelled out rather than compared against - // `mock_genesis::chain_set()`, because the TypeScript mock declares - // these same three and a suite written against either has to be - // answered the same way. - let p = MockPlatform::new(); - let default_set = block_on(p.supported_chains()).expect("default set"); - assert_eq!(default_set.network, "mock"); +fn the_mock_stack_names_the_chains_the_mock_serves() { + // Spelled out rather than compared against `mock_genesis`, because the + // TypeScript mock declares these same three and a suite written against + // either has to be answered the same way. + let stack = mock_genesis::stack(); assert_eq!( - default_set - .chains - .iter() - .map(|entry| (entry.identifier, entry.genesis_hash)) - .collect::>(), - vec![ - (latest::ChainIdentifier::People, [0x11; 32]), - (latest::ChainIdentifier::Bulletin, [0x22; 32]), - (latest::ChainIdentifier::AssetHub, [0x33; 32]), - ] + (stack.network(), stack.tld(), stack.chains().clone()), + ( + "mock", + "paseo", + crate::platform::StackChains { + relay: None, + asset_hub: Some([0x33; 32]), + people: [0x11; 32], + bulletin: [0x22; 32], + } + ) ); - - let p = MockPlatform::with_config(MockConfig { - supported_chains: crate::platform::HostChainSet { - network: "paseo".to_string(), - chains: vec![crate::platform::HostChainEntry { - identifier: latest::ChainIdentifier::AssetHub, - genesis_hash: [0xaa; 32], - }], - }, - ..MockConfig::default() - }); - let set = block_on(p.supported_chains()).expect("declared set"); - assert_eq!(set.network, "paseo"); - assert_eq!(set.chains.len(), 1); - assert_eq!(set.chains[0].genesis_hash, [0xaa; 32]); + assert_eq!(MockConfig::default().served_chains, vec![[0x11; 32], [0x22; 32], [0x33; 32]]); } #[test] diff --git a/rust/crates/truapi/src/platform/tests.rs b/rust/crates/truapi/src/platform/tests.rs index 235a0145b0..0b733bbe99 100644 --- a/rust/crates/truapi/src/platform/tests.rs +++ b/rust/crates/truapi/src/platform/tests.rs @@ -1,47 +1,5 @@ use super::*; -fn signing_host_config( - network_suffix: &str, -) -> Result { - SigningHostConfig::new( - HostInfo { - name: "Test host".to_string(), - icon: None, - version: None, - platform: HostPlatform::Unknown, - }, - PlatformInfo::default(), - [0; 32], - [1; 32], - [2; 32], - network_suffix.to_string(), - ) -} - -#[test] -fn a_signing_host_is_configured_for_one_dotns_tld() { - for tld in DOTNS_TLDS { - let config = signing_host_config(tld).expect("a known TLD is a valid suffix"); - assert_eq!(config.network_suffix, *tld); - } - - assert_eq!( - signing_host_config(""), - Err(RuntimeConfigValidationError::EmptyField { field: "network_suffix" }) - ); - for malformed in - ["pasoe", "unknown", ".paseo", "peopl.paseo", "Paseo", "pas eo", "a-b", "abcdefghijklmnopq"] - { - assert_eq!( - signing_host_config(malformed), - Err(RuntimeConfigValidationError::InvalidNetworkSuffix { - network_suffix: malformed.to_string() - }), - "{malformed:?} must be rejected" - ); - } -} - fn file_with_url(url: &str) -> ChatMessageContent { ChatMessageContent::File(ChatFile { url: url.to_string(), diff --git a/rust/crates/truapi/src/runtime.rs b/rust/crates/truapi/src/runtime.rs index 6615cd441c..15a7ea9a7a 100644 --- a/rust/crates/truapi/src/runtime.rs +++ b/rust/crates/truapi/src/runtime.rs @@ -762,9 +762,7 @@ impl ProductRuntimeHost { platform: truapi::latest::HostPlatform::Web, }, crate::platform::PlatformInfo::default(), - [0; 32], - [0xbb; 32], - [0xcc; 32], + crate::test_support::test_stack("paseo", [0; 32], [0xbb; 32], Some([0xcc; 32])), "polkadotapp".to_string(), ) .expect("compat runtime config is valid") @@ -806,9 +804,7 @@ impl ProductRuntimeHost { let services = RuntimeServices::new( platform.clone(), host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), spawner.clone(), ); let pairing_host = PairingHost::new(services.clone(), host_config); diff --git a/rust/crates/truapi/src/runtime/capabilities/chain.rs b/rust/crates/truapi/src/runtime/capabilities/chain.rs index 3de75de48a..76bc6abebf 100644 --- a/rust/crates/truapi/src/runtime/capabilities/chain.rs +++ b/rust/crates/truapi/src/runtime/capabilities/chain.rs @@ -32,12 +32,9 @@ use truapi::{ }, }; -use crate::{ - host_logic::features::{chain_info, supported_chains}, - runtime::{ - AUTHORITY_CANCEL_UNWIND_GRACE, PERMISSION_DENIED_REASON, ProductConnection, - ProductRuntimeHost, runtime_failure_to_call_error, versioning::version, - }, +use crate::runtime::{ + AUTHORITY_CANCEL_UNWIND_GRACE, PERMISSION_DENIED_REASON, ProductConnection, ProductRuntimeHost, + runtime_failure_to_call_error, versioning::version, }; impl ProductConnection { @@ -336,10 +333,7 @@ impl Chain for ProductRuntimeHost { ) -> Result> { version(request) .exec(|inner| async move { - let set = supported_chains(self.connection.services.platform.as_ref()) - .await - .map_err(|err| CallError::Domain(latest::RemoteChainInfoError::Unknown(err)))?; - chain_info(&set, &inner).map_err(CallError::Domain) + self.connection.services.stack().chain_info(&inner).map_err(CallError::Domain) }) .await } diff --git a/rust/crates/truapi/src/runtime/pairing_host/tests.rs b/rust/crates/truapi/src/runtime/pairing_host/tests.rs index 04977d43ff..a6712723ae 100644 --- a/rust/crates/truapi/src/runtime/pairing_host/tests.rs +++ b/rust/crates/truapi/src/runtime/pairing_host/tests.rs @@ -44,9 +44,7 @@ fn a_withdrawn_request_already_published_is_cancelled_on_the_phone() { let services = RuntimeServices::new( platform.clone(), host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), test_spawner(), ); let pairing_host = PairingHost::new(services.clone(), host_config); @@ -101,9 +99,7 @@ fn a_request_that_times_out_is_not_withdrawn_from_the_phone() { let services = RuntimeServices::new( platform.clone(), host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), test_spawner(), ); let pairing_host = PairingHost::new(services.clone(), host_config); @@ -151,9 +147,7 @@ fn a_withdrawn_request_with_a_newer_one_behind_it_sends_no_cancel() { let services = RuntimeServices::new( platform.clone(), host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), test_spawner(), ); let pairing_host = PairingHost::new(services.clone(), host_config); diff --git a/rust/crates/truapi/src/runtime/product_manifest/tests.rs b/rust/crates/truapi/src/runtime/product_manifest/tests.rs index a0e846c8af..0409a5f62e 100644 --- a/rust/crates/truapi/src/runtime/product_manifest/tests.rs +++ b/rust/crates/truapi/src/runtime/product_manifest/tests.rs @@ -57,7 +57,7 @@ fn a_worker_subname_is_the_namehash_of_its_full_name() { #[test] fn an_uncached_lookup_reports_why_it_read_no_manifest() { let lookup = |platform: crate::test_support::StubPlatform, - asset_hub_chain_genesis_hash: [u8; 32], + asset_hub: Option<[u8; 32]>, ceiling: Duration| { let platform = std::sync::Arc::new(platform); let services = RuntimeServices::new( @@ -68,9 +68,7 @@ fn an_uncached_lookup_reports_why_it_read_no_manifest() { version: None, platform: truapi::latest::HostPlatform::Unknown, }, - [0; 32], - [0xbb; 32], - asset_hub_chain_genesis_hash, + crate::test_support::test_stack("paseo", [0; 32], [0xbb; 32], asset_hub), crate::test_support::test_spawner(), ); futures::executor::block_on(root_manifest_within( @@ -82,13 +80,13 @@ fn an_uncached_lookup_reports_why_it_read_no_manifest() { }; assert_eq!( [ - lookup(Default::default(), [0; 32], MANIFEST_RESOLUTION_CEILING), + lookup(Default::default(), None, MANIFEST_RESOLUTION_CEILING), lookup( crate::test_support::StubPlatform { chain_connect_error: Some("fixture serves no chain"), ..Default::default() }, - [0xcc; 32], + Some([0xcc; 32]), MANIFEST_RESOLUTION_CEILING, ), lookup( @@ -96,7 +94,7 @@ fn an_uncached_lookup_reports_why_it_read_no_manifest() { chain_connect_pending: true, ..Default::default() }, - [0xcc; 32], + Some([0xcc; 32]), Duration::from_millis(10), ), ], diff --git a/rust/crates/truapi/src/runtime/services.rs b/rust/crates/truapi/src/runtime/services.rs index 6001d1649d..0c3654ee46 100644 --- a/rust/crates/truapi/src/runtime/services.rs +++ b/rust/crates/truapi/src/runtime/services.rs @@ -20,7 +20,7 @@ use crate::{ host_logic::worker::WorkerLedger, platform::{ BalancePlatform, ChatPlatform, ContactsPlatform, GamePlatform, HostInfo, PaymentPlatform, - PermissionStatusHost, Platform, PocketPlatform, ScannerPlatform, TopUpPlatform, + PermissionStatusHost, Platform, PocketPlatform, ScannerPlatform, Stack, TopUpPlatform, }, runtime::{ bulletin_rpc::BulletinRpc, sso_responder_service::DevicePairingObserver, @@ -121,9 +121,8 @@ pub struct RuntimeServices { /// recovery to whoever calls it. #[cfg(not(target_arch = "wasm32"))] durable_work_observer: OnceLock>, - /// Asset Hub the dotNS contracts are deployed on. All-zero says this host - /// has none, which leaves every manifest unresolvable. - asset_hub_chain_genesis_hash: [u8; 32], + /// The stack the host runs, fixed for the life of the runtime. + stack: Stack, /// Reference counts per product worker. pub worker_ledger: WorkerLedger, /// Shared chainHead-v1 runtime behind the Chain surface. @@ -159,39 +158,23 @@ pub struct RuntimeServices { impl RuntimeServices { /// Build role-neutral runtime services from the platform, the host - /// identity reported to products, the People-chain genesis hash used by - /// statement-store backed protocols, the Bulletin-chain genesis hash used - /// for in-core preimage submission, and the Asset Hub genesis hash product - /// manifests are resolved from. - /// - /// The three genesis hashes are adjacent and same-typed, so a transposition - /// compiles. Each call site is pinned by its own test. + /// identity reported to products, and the stack whose People chain backs + /// the statement store, whose Bulletin chain takes in-core preimage + /// submissions, and whose Asset Hub product manifests resolve from. pub fn new( platform: Arc, host_info: HostInfo, - people_chain_genesis_hash: [u8; 32], - bulletin_chain_genesis_hash: [u8; 32], - asset_hub_chain_genesis_hash: [u8; 32], + stack: Stack, spawner: Spawner, ) -> Arc { - Self::with_chat_platform( - platform, - host_info, - people_chain_genesis_hash, - bulletin_chain_genesis_hash, - asset_hub_chain_genesis_hash, - spawner, - None, - ) + Self::with_chat_platform(platform, host_info, stack, spawner, None) } /// Same as [`Self::new`], with the host's chat adapter installed. pub fn with_chat_platform( platform: Arc, host_info: HostInfo, - people_chain_genesis_hash: [u8; 32], - bulletin_chain_genesis_hash: [u8; 32], - asset_hub_chain_genesis_hash: [u8; 32], + stack: Stack, spawner: Spawner, chat_platform: Option>, ) -> Arc { @@ -200,12 +183,11 @@ impl RuntimeServices { let statement_store = StatementStoreRpc::new( platform.clone(), chain.clone(), - people_chain_genesis_hash, + stack.chains().people, spawner.clone(), ); let extrinsics = Arc::new(ExtrinsicService::over_chain(chain.clone(), platform.clone())); - let bulletin = - BulletinRpc::new(chain.clone(), extrinsics.clone(), bulletin_chain_genesis_hash); + let bulletin = BulletinRpc::new(chain.clone(), extrinsics.clone(), stack.chains().bulletin); // The funding registry follows sessions through the services it // belongs to, so it is built holding them. Arc::new_cyclic(|services| Self { @@ -242,7 +224,7 @@ impl RuntimeServices { encrypted_storage: OnceLock::new(), #[cfg(not(target_arch = "wasm32"))] durable_work_observer: OnceLock::new(), - asset_hub_chain_genesis_hash, + stack, worker_ledger: WorkerLedger::default(), chain, extrinsics, @@ -267,20 +249,18 @@ impl RuntimeServices { self.permission_status.set(host).is_ok() } - /// The Asset Hub dotNS reads run against, when one is configured. - /// - /// Taken by construction, so the chain a grant is adjudicated against - /// cannot move under a running product. It is deliberately not sourced from - /// `supported_chains()`, which is an uncached per-call host syscall - /// answering a different question, "which chains do I serve RPC for?", the - /// product-facing `get_chain_info` advertisement, rather than "which Asset - /// Hub is dotNS deployed on?". Taking it from there would let the anchor - /// change between two calls, at host discretion, with nothing recording it. + /// The stack the host runs. + pub fn stack(&self) -> &Stack { + &self.stack + } + + /// The Asset Hub dotNS reads run against, when the stack has one. /// - /// An all-zero hash is how a host says it has no Asset Hub. `None` fails - /// every manifest lookup closed: grants are refused rather than assumed. + /// Fixed by construction, so the chain a grant is adjudicated against + /// cannot move under a running product. `None` fails every manifest lookup + /// closed: grants are refused rather than assumed. pub fn asset_hub_chain_genesis_hash(&self) -> Option<[u8; 32]> { - Some(self.asset_hub_chain_genesis_hash).filter(|hash| *hash != [0u8; 32]) + self.stack.chains().asset_hub } /// The host's live OS permission-status adapter, when one is installed. diff --git a/rust/crates/truapi/src/runtime/signing_host.rs b/rust/crates/truapi/src/runtime/signing_host.rs index 0fd7d5e646..647f28dd91 100644 --- a/rust/crates/truapi/src/runtime/signing_host.rs +++ b/rust/crates/truapi/src/runtime/signing_host.rs @@ -11,7 +11,7 @@ use std::{ sync::{Arc, Mutex}, }; use truapi::latest::{ - ChainIdentifier, DerivationIndex, HostAccountCreateProofRequest, HostAccountGetAliasRequest, + DerivationIndex, HostAccountCreateProofRequest, HostAccountGetAliasRequest, HostAccountListRingVrfKeysRequest, HostAccountRegisterRingVrfKeyRequest, HostAccountRingVrfSignRequest, ProductAccountId, RingLocation, RingLocationJunction, }; @@ -44,7 +44,6 @@ use crate::{ }, host_logic::{ entropy::derive_product_entropy, - features::genesis_for, product_account::{ SR25519_SIGNING_CONTEXT, derivation_index_bytes, derive_full_person_ring_vrf_entropy, derive_identity_keypair, derive_lite_person_ring_vrf_entropy, derive_product_keypair, @@ -151,10 +150,6 @@ impl LocalGrantState { pub struct SigningHost { services: Arc, platform: Arc, - /// The dotNS TLD of the network this wallet serves, from - /// [`crate::platform::SigningHostConfig::network_suffix`]. Every reserved - /// RFC-0022 derivation (`uid.`, `peopl.`) ends in it. - network_suffix: String, session_state: Arc, auth_state: AuthStateMachine, /// Account-access decisions and reviews for local and paired callers. @@ -184,14 +179,13 @@ pub struct SigningHost { } impl SigningHost { - /// Build a locked signing host for the network's dotNS suffix. - pub fn new(services: Arc, network_suffix: String) -> Arc { + /// Build a locked signing host for the stack `services` runs. + pub fn new(services: Arc) -> Arc { let platform = services.platform.clone(); let ring_resolver = ChainRingResolver::new(services.chain.clone()); Arc::new(Self { services, platform: platform.clone(), - network_suffix, #[cfg(feature = "test-host")] resource_controls: Arc::default(), #[cfg(feature = "test-host")] @@ -264,15 +258,12 @@ impl SigningHost { version: None, platform: truapi::latest::HostPlatform::Unknown, }, - [0; 32], - [0xbb; 32], - [0xcc; 32], + crate::test_support::test_stack(network_suffix, [0; 32], [0xbb; 32], Some([0xcc; 32])), crate::test_support::test_spawner(), ); Arc::new(Self { services, platform: platform.clone(), - network_suffix: network_suffix.to_string(), #[cfg(feature = "test-host")] resource_controls: Arc::default(), #[cfg(feature = "test-host")] @@ -294,10 +285,10 @@ impl SigningHost { self.session_state.clone() } - /// The dotNS TLD of the network this wallet serves: the suffix of every + /// The dotNS TLD of the stack this wallet serves: the suffix of every /// reserved identity it derives. pub fn network_suffix(&self) -> &str { - &self.network_suffix + self.services.stack().tld() } /// Current root entropy, or [`AuthorityError::Disconnected`] when no local @@ -443,7 +434,7 @@ impl SigningHost { fn identity_keypair(&self) -> Result { let entropy = self.root_entropy()?; - derive_identity_keypair(&entropy, &self.network_suffix).map_err(Into::into) + derive_identity_keypair(&entropy, self.network_suffix()).map_err(Into::into) } fn install_local_session(&self, secret: Zeroizing>, session: SessionInfo) { @@ -518,11 +509,11 @@ impl SigningHost { Ok(vec![ CollectionCandidate { collection: PersonhoodCollection::People, - entropy: derive_full_person_ring_vrf_entropy(&root, &self.network_suffix), + entropy: derive_full_person_ring_vrf_entropy(&root, self.network_suffix()), }, CollectionCandidate { collection: PersonhoodCollection::LitePeople, - entropy: derive_lite_person_ring_vrf_entropy(&root, &self.network_suffix), + entropy: derive_lite_person_ring_vrf_entropy(&root, self.network_suffix()), }, ]) } @@ -532,16 +523,10 @@ impl SigningHost { session: &AuthoritySession, owner: &ProductId, ) -> Result<(), RingVrfError> { - if owner != &personhood_product_id(&self.network_suffix) { + if owner != &personhood_product_id(self.network_suffix()) { return Ok(()); } - let chains = self - .platform - .supported_chains() - .await - .map_err(|error| RingVrfError::Unknown { reason: error.reason })?; - let chain_id = - genesis_for(&chains, ChainIdentifier::People).ok_or(RingVrfError::RingNotFound)?; + let chain_id = self.services.stack().chains().people; let entries = self.ring_vrf_registry.owner_entries(session.public_key, owner).await?; let missing = [(PersonhoodCollection::People, 0), (PersonhoodCollection::LitePeople, 1)] .into_iter() diff --git a/rust/crates/truapi/src/runtime/signing_host/allowance.rs b/rust/crates/truapi/src/runtime/signing_host/allowance.rs index bb6d458793..03705f254b 100644 --- a/rust/crates/truapi/src/runtime/signing_host/allowance.rs +++ b/rust/crates/truapi/src/runtime/signing_host/allowance.rs @@ -27,15 +27,12 @@ pub enum AllowanceAllocationError { /// Signing host session or authority state was unavailable. #[error("{0}")] Authority(#[from] AuthorityError), - /// The host serves no chain for this role, so there is nothing to claim on. - #[error("host serves no {chain} chain")] + /// The stack has no chain for this role, so there is nothing to claim on. + #[error("the stack has no {chain} chain")] ChainNotServed { /// Role that could not be resolved. chain: &'static str, }, - /// Reading the host's chain set failed. - #[error("supported chains: {0}")] - SupportedChains(String), /// Product-account key derivation failed. #[error("{0}")] ProductAccount(#[from] ProductAccountError), @@ -336,12 +333,7 @@ pub async fn allocate_smart_contract_allowance( derivation_index: api::DerivationIndex, policy: OnExistingAllowancePolicy, ) -> Result<(), AllowanceAllocationError> { - use truapi::latest::ChainIdentifier; - - use crate::{ - host_logic::features, - runtime::statement_allowance::{self, ChainClient, find_including_rings, pgas}, - }; + use crate::runtime::statement_allowance::{self, ChainClient, find_including_rings, pgas}; signing_host.require_current_session(session)?; @@ -354,10 +346,8 @@ pub async fn allocate_smart_contract_allowance( .public .to_bytes(); - let chains = features::supported_chains(services.platform.as_ref()) - .await - .map_err(|err| AllowanceAllocationError::SupportedChains(err.reason))?; - let asset_hub_genesis = features::genesis_for(&chains, ChainIdentifier::AssetHub) + let asset_hub_genesis = services + .asset_hub_chain_genesis_hash() .ok_or(AllowanceAllocationError::ChainNotServed { chain: "Asset Hub" })?; let asset_hub_client = ChainClient::new( statement_allowance::rpc::RpcClient::new(subxt_rpcs::RpcClient::new( diff --git a/rust/crates/truapi/src/runtime/signing_host/tests.rs b/rust/crates/truapi/src/runtime/signing_host/tests.rs index 362836eac4..56609accbd 100644 --- a/rust/crates/truapi/src/runtime/signing_host/tests.rs +++ b/rust/crates/truapi/src/runtime/signing_host/tests.rs @@ -160,21 +160,21 @@ fn signing_runtime_on_asset_hub( platform: truapi::latest::HostPlatform::Ios, }, PlatformInfo::default(), - [0; 32], - [0xbb; 32], - asset_hub_chain_genesis_hash, - TEST_NETWORK_SUFFIX.to_string(), + crate::test_support::test_stack( + TEST_NETWORK_SUFFIX, + [0; 32], + [0xbb; 32], + Some(asset_hub_chain_genesis_hash), + ), ) .expect("signing host config is valid"); let services = RuntimeServices::new( platform.clone(), config.host.host_info.clone(), - config.people_chain_genesis_hash, - config.bulletin_chain_genesis_hash, - config.asset_hub_chain_genesis_hash, + config.stack.clone(), test_spawner(), ); - let signing_host = SigningHostRole::new(services.clone(), config.network_suffix); + let signing_host = SigningHostRole::new(services.clone()); (services, signing_host) } diff --git a/rust/crates/truapi/src/runtime/sso_request_service.rs b/rust/crates/truapi/src/runtime/sso_request_service.rs index 38e7530f2e..12806e2ad2 100644 --- a/rust/crates/truapi/src/runtime/sso_request_service.rs +++ b/rust/crates/truapi/src/runtime/sso_request_service.rs @@ -299,7 +299,7 @@ impl SsoRequestService { let activation_epoch = self.advance_session_lifecycle(); let resolved = resolve_session_identity_with_chain( &self.chain, - self.host_config.asset_hub_chain_genesis_hash, + self.host_config.stack.chains().asset_hub.unwrap_or_default(), session, ) .await; @@ -356,7 +356,7 @@ impl SsoRequestService { }; let resolved = resolve_session_identity_with_chain( &self.chain, - self.host_config.asset_hub_chain_genesis_hash, + self.host_config.stack.chains().asset_hub.unwrap_or_default(), session, ) .await; @@ -605,13 +605,13 @@ impl SsoRequestService { async fn refresh_current_session_identity(&self) -> Option { let current = self.session_state.current()?; - if current.has_username() || self.host_config.asset_hub_chain_genesis_hash == [0; 32] { + if current.has_username() || self.host_config.stack.chains().asset_hub.is_none() { return Some(authority_session(¤t)); } let resolved = resolve_session_identity_with_chain( &self.chain, - self.host_config.asset_hub_chain_genesis_hash, + self.host_config.stack.chains().asset_hub.unwrap_or_default(), current.clone(), ) .await; diff --git a/rust/crates/truapi/src/runtime/sso_request_service/pairing.rs b/rust/crates/truapi/src/runtime/sso_request_service/pairing.rs index 223e5b3b9f..a9f1545ea7 100644 --- a/rust/crates/truapi/src/runtime/sso_request_service/pairing.rs +++ b/rust/crates/truapi/src/runtime/sso_request_service/pairing.rs @@ -259,7 +259,7 @@ impl<'a> SsoPairingFlow<'a> { }; let resolve_session = resolve_session_identity_with_chain( &self.host.chain, - self.host.host_config.asset_hub_chain_genesis_hash, + self.host.host_config.stack.chains().asset_hub.unwrap_or_default(), session, ) .fuse(); diff --git a/rust/crates/truapi/src/runtime/sso_request_service/pairing/tests.rs b/rust/crates/truapi/src/runtime/sso_request_service/pairing/tests.rs index 3f74f354ba..5fcc3bc263 100644 --- a/rust/crates/truapi/src/runtime/sso_request_service/pairing/tests.rs +++ b/rust/crates/truapi/src/runtime/sso_request_service/pairing/tests.rs @@ -497,8 +497,7 @@ fn request_login_accepts_pairing_statement_from_snapshot_query_page() { ..Default::default() }); let connection = - futures::executor::block_on(platform.connect(host_config.people_chain_genesis_hash)) - .unwrap(); + futures::executor::block_on(platform.connect(host_config.stack.chains().people)).unwrap(); let rpc_client = RpcClient::new(HostRpcClient::new(Arc::from(connection), test_spawner())); let progress = futures::executor::block_on(run_pairing_snapshot_query( rpc_client, diff --git a/rust/crates/truapi/src/runtime/sso_responder_service/tests.rs b/rust/crates/truapi/src/runtime/sso_responder_service/tests.rs index bbf4607c42..f248a4bc96 100644 --- a/rust/crates/truapi/src/runtime/sso_responder_service/tests.rs +++ b/rust/crates/truapi/src/runtime/sso_responder_service/tests.rs @@ -68,22 +68,16 @@ fn signing_fixture(platform: Arc) -> (Arc, Arc, host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), test_spawner(), None, ); @@ -879,9 +884,7 @@ fn a_host_that_only_resolves_contacts_reports_unsupported() { let services = RuntimeServices::with_chat_platform( stub_platform() as Arc, host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), test_spawner(), None, ); @@ -1201,9 +1204,7 @@ fn host_with_contacts(contacts: Arc) -> P let services = RuntimeServices::with_chat_platform( stub_platform() as Arc, host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), test_spawner(), None, ); @@ -1502,9 +1503,7 @@ fn chat_post_message_screens_content_before_it_reaches_a_host() { let services = RuntimeServices::new( platform.clone(), host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), spawner.clone(), ); let chat_platform = Arc::new(RecordingChatPlatform::default()); @@ -1649,9 +1648,7 @@ fn a_room_footer_reaches_the_host_for_the_room_the_product_created() { let services = RuntimeServices::new( platform.clone(), host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), spawner.clone(), ); let chat_platform = Arc::new(RecordingChatPlatform { @@ -1758,9 +1755,7 @@ fn chat_post_message_screens_the_alt_before_it_reaches_a_host() { let services = RuntimeServices::new( platform.clone(), host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), spawner.clone(), ); let chat_platform = Arc::new(RecordingChatPlatform::default()); @@ -1840,9 +1835,7 @@ fn chat_room_ids_agree_across_create_and_post() { let services = RuntimeServices::new( platform.clone(), host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), spawner.clone(), ); let chat_platform = Arc::new(RecordingChatPlatform::default()); @@ -1924,9 +1917,7 @@ fn chat_register_bot_rejects_unsafe_product_fields() { let services = RuntimeServices::new( platform.clone(), host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), spawner.clone(), ); let chat_platform = Arc::new(RecordingChatPlatform::default()); @@ -2013,9 +2004,7 @@ fn chat_register_bot_reaches_the_installed_adapter() { let services = RuntimeServices::new( platform.clone(), host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), spawner.clone(), ); let chat_platform = Arc::new(RecordingChatPlatform::default()); @@ -2115,9 +2104,7 @@ fn pocket_dot_host( let services = RuntimeServices::new( platform, host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), test_spawner(), ); let pairing_host = PairingHost::new(services.clone(), host_config); @@ -2459,9 +2446,7 @@ fn game_host_for( let services = RuntimeServices::new( platform, host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), test_spawner(), ); let pairing_host = PairingHost::new(services.clone(), host_config); @@ -2729,9 +2714,7 @@ fn funding_services_with( let services = RuntimeServices::new( platform, host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), spawner, ); assert!(services.install_core_db(db.clone())); @@ -3368,9 +3351,7 @@ fn chain_follow_ids_are_scoped_per_product_core() { let services = RuntimeServices::new( platform.clone(), host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), spawner.clone(), ); let pairing_host = PairingHost::new(services.clone(), host_config); @@ -3478,9 +3459,7 @@ fn permission_prompts_name_the_requesting_product_and_execution_kind() { let services = RuntimeServices::new( platform.clone(), host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), spawner, ); let adapters = crate::host_core::ConnectionAdapters::from_services(&services); @@ -7064,9 +7043,7 @@ fn the_pairing_authority_refuses_a_foreign_ring_vrf_key_without_a_grant() { let services = RuntimeServices::new( platform.clone(), host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), test_spawner(), ); let pairing_host = PairingHost::new(services.clone(), host_config); @@ -7148,9 +7125,7 @@ fn the_pairing_authority_admits_a_context_only_its_product_granted() { let services = RuntimeServices::new( platform.clone(), host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), test_spawner(), ); let pairing_host = PairingHost::new(services.clone(), host_config); @@ -7217,9 +7192,7 @@ fn a_grant_lookup_obeys_the_callers_deadline() { let services = RuntimeServices::new( platform.clone(), host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), test_spawner(), ); let pairing_host = PairingHost::new(services.clone(), host_config); @@ -7271,7 +7244,7 @@ fn a_grant_lookup_obeys_the_callers_deadline() { /// say why. #[test] fn a_frontend_refusal_tells_the_host_what_it_judged_by() { - let host = dim2_with_a_pairing_session(stub_platform(), [0; 32]); + let host = dim2_with_a_pairing_session(stub_platform(), None); let cx = CallContext::default(); let mut refusals = None; let lines = crate::logging::testing::host_sink_lines(|| { @@ -7311,7 +7284,7 @@ fn a_frontend_lookup_past_its_deadline_tells_the_host_it_timed_out() { let proof_and_signature_lines = |cx: CallContext| { let platform = Arc::new(StubPlatform { chain_connect_pending: true, ..StubPlatform::default() }); - let host = dim2_with_a_pairing_session(platform, [0xcc; 32]); + let host = dim2_with_a_pairing_session(platform, Some([0xcc; 32])); crate::logging::testing::host_sink_lines(|| { let _ = futures::executor::block_on( host.create_account_proof(&cx, create_proof_request("peopl.dot")), @@ -7341,7 +7314,7 @@ fn a_frontend_lookup_past_its_deadline_tells_the_host_it_timed_out() { fn a_refusal_past_the_frontend_is_warned_once() { let platform = stub_platform(); cache_manifest(&platform, "peopl.dot", r#"{"dim2":["context"]}"#, 0); - let host = dim2_with_a_pairing_session(platform, [0; 32]); + let host = dim2_with_a_pairing_session(platform, None); let HostAccountCreateProofRequest::V1(mut request) = create_proof_request("peopl.dot"); request.context.product_id = ProductId::for_test("dim2.paseo"); let lines = crate::logging::testing::host_sink_lines(|| { @@ -7361,18 +7334,21 @@ fn a_refusal_past_the_frontend_is_warned_once() { } /// `dim2.dot` with a live pairing session, resolving manifests against -/// `asset_hub_chain_genesis_hash`. +/// `asset_hub`. fn dim2_with_a_pairing_session( platform: Arc, - asset_hub_chain_genesis_hash: [u8; 32], + asset_hub: Option<[u8; 32]>, ) -> ProductRuntimeHost { let (host_config, product) = runtime_config("dim2.dot"); let services = RuntimeServices::new( platform, host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - asset_hub_chain_genesis_hash, + crate::test_support::test_stack( + "paseo", + host_config.stack.chains().people, + host_config.stack.chains().bulletin, + asset_hub, + ), test_spawner(), ); let pairing_host = PairingHost::new(services.clone(), host_config); @@ -7563,9 +7539,7 @@ fn scanner_host_for( let services = RuntimeServices::with_chat_platform( stub_platform() as Arc, host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), test_spawner(), None, ); @@ -7840,9 +7814,7 @@ fn payment_services_over(platform: Arc) -> Arc { RuntimeServices::new( platform, host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), test_spawner(), ) } diff --git a/rust/crates/truapi/src/stack.rs b/rust/crates/truapi/src/stack.rs new file mode 100644 index 0000000000..9af7c3d8fb --- /dev/null +++ b/rust/crates/truapi/src/stack.rs @@ -0,0 +1,215 @@ +//! The Trinity stack a host runs, the core's only source of chain +//! configuration. +//! +//! A host build bundles a signed descriptor and hands it to +//! [`Stack::bundled`] with the stack's pointer. The resolved [`Stack`] answers +//! `get_chain_info`, names the People, Bulletin and Asset Hub chains the core +//! talks to, and carries the TLD the signing role derives identities under. + +use trinity_stack::{Pointer, open}; +use truapi::latest::{ + ChainIdentifier, RemoteChainInfoError, RemoteChainInfoRequest, RemoteChainInfoResponse, +}; + +use crate::dotns_name::DOTNS_TLDS; + +/// Polkadot's genesis hashes for the chain roles the core reads, Relay, Asset +/// Hub, People and Bulletin, in lowercase hex as PCF's signed Polkadot +/// descriptor lists them. +const RESERVED_GENESIS_HASHES: [&str; 4] = [ + "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", + "68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f", + "67fa177a097bfa18f77ea95ab56e9bcdfeb0e5b8a40e46298bb93e16b6fc5008", + "2761c95259d59e55ae3daf756c1413b46e45a5a2987299f8ef8e5d8e4776cbc4", +]; + +/// Polkadot's dotNS TLD. +const RESERVED_TLDS: [&str; 1] = ["dot"]; + +/// The stack a host runs: the network `get_chain_info` reports, the dotNS TLD, +/// and the genesis hash of each chain role the core uses. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Stack { + network: String, + tld: String, + chains: StackChains, +} + +/// The genesis hash of each chain role a stack names. People and Bulletin are +/// required: the core runs SSO and its own preimage submission over them. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StackChains { + /// The relay chain. + pub relay: Option<[u8; 32]>, + /// Asset Hub, where the dotNS contracts are deployed. Without one, product + /// manifests and session usernames do not resolve. + pub asset_hub: Option<[u8; 32]>, + /// The People chain. + pub people: [u8; 32], + /// The Bulletin chain. + pub bulletin: [u8; 32], +} + +/// Why a stack was refused. +#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display, derive_more::Error)] +pub enum StackError { + /// The pointer is not a stack link. + #[display("stack pointer is not a stack link: {_0}")] + Pointer(#[error(not(source))] trinity_stack::Error), + /// The pointer carries no key. A host refreshes its stack unseen, so the + /// stack must be signed. + #[display("stack pointer has no key; a host's stack must be signed")] + Unsigned, + /// The bundled descriptor breaks a rule or does not verify under the + /// pointer's key. + #[display("bundled stack descriptor refused ({}): {_0}", _0.code())] + Descriptor(#[error(not(source))] trinity_stack::Error), + /// The descriptor has no chain with this role. + #[display("stack has no {role} chain")] + MissingChain { + /// The missing role, as descriptors name it. + role: &'static str, + }, + /// The TLD is not one the core serves dotNS names under. + #[display("stack tld {tld:?} is not one of {}", DOTNS_TLDS.join(", "))] + UnsupportedTld { + /// The descriptor's TLD. + tld: String, + }, + /// The stack uses one of Polkadot's genesis hashes or its TLD, and the host + /// build does not allow this stack to. + #[display("stack uses Polkadot's reserved {value}, which this build does not allow")] + Reserved { + /// The reserved value, as `genesis hash 0x…` or `tld dot`. + value: String, + }, +} + +impl Stack { + /// The stack a host build bundles: `pointer` is the stack's link, such as + /// `polkadotapp://stack?key=0x…&url=…`, and `descriptor` the bytes of the + /// copy the build verified. + /// + /// Fails when the pointer has no key, when the descriptor does not verify + /// under it, when it has no People or Bulletin chain, when its TLD is not a + /// dotNS TLD the core serves, or when it names a reserved value and + /// `allows_reserved_values` is false. Expiry is not checked: a copy past + /// `expiresAt` still starts, since nothing fetches a newer one yet. + pub fn bundled( + pointer: &str, + descriptor: &[u8], + allows_reserved_values: bool, + ) -> Result { + let pointer = Pointer::from_link(pointer).map_err(StackError::Pointer)?; + if pointer.key().is_none() { + return Err(StackError::Unsigned); + } + let descriptor = open(descriptor, &pointer).map_err(StackError::Descriptor)?.descriptor; + let genesis_of = + |role: &str| descriptor.chain(role).map(|chain| genesis(&chain.genesis_hash)); + if !allows_reserved_values { + for chain in &descriptor.chains { + refuse_reserved_genesis(genesis(&chain.genesis_hash))?; + } + } + let chains = StackChains { + relay: genesis_of("Relay"), + asset_hub: genesis_of("AssetHub"), + people: genesis_of("People").ok_or(StackError::MissingChain { role: "People" })?, + bulletin: genesis_of("Bulletin") + .ok_or(StackError::MissingChain { role: "Bulletin" })?, + }; + Self::new(descriptor.network, descriptor.tld, chains, allows_reserved_values) + } + + /// A stack from values a Rust host resolved itself, such as the CLI from a + /// local-network file. Native and browser hosts reach only + /// [`Self::bundled`]. + /// + /// Applies the same TLD and reserved-value rules as [`Self::bundled`]. + pub fn new( + network: String, + tld: String, + chains: StackChains, + allows_reserved_values: bool, + ) -> Result { + if !DOTNS_TLDS.contains(&tld.as_str()) { + return Err(StackError::UnsupportedTld { tld }); + } + if !allows_reserved_values { + if RESERVED_TLDS.contains(&tld.as_str()) { + return Err(StackError::Reserved { value: format!("tld {tld}") }); + } + let StackChains { relay, asset_hub, people, bulletin } = &chains; + for hash in [relay, asset_hub].into_iter().flatten().chain([people, bulletin]) { + refuse_reserved_genesis(*hash)?; + } + } + Ok(Self { network, tld, chains }) + } + + /// The ecosystem `get_chain_info` reports, such as `polkadot` or `paseo`. + pub fn network(&self) -> &str { + &self.network + } + + /// The dotNS TLD without the leading dot. The signing role derives every + /// reserved RFC-0022 identity under it: `uid.` for the identity + /// account, `peopl.` for the person ring-VRF keys. + pub fn tld(&self) -> &str { + &self.tld + } + + /// The stack's chains. + pub fn chains(&self) -> &StackChains { + &self.chains + } + + /// The genesis hash of the chain with this role, or `None` when the stack + /// has no such chain. + pub fn genesis_hash(&self, chain: ChainIdentifier) -> Option<[u8; 32]> { + let StackChains { relay, asset_hub, people, bulletin } = self.chains; + match chain { + ChainIdentifier::Relay => relay, + ChainIdentifier::AssetHub => asset_hub, + ChainIdentifier::People => Some(people), + ChainIdentifier::Bulletin => Some(bulletin), + } + } + + /// Answers `get_chain_info` (RFC 0026): the requested chain's genesis hash + /// and the stack's network, or `NotSupported` when the stack has no such + /// chain. + pub fn chain_info( + &self, + request: &RemoteChainInfoRequest, + ) -> Result { + self.genesis_hash(request.chain) + .map(|genesis_hash| RemoteChainInfoResponse { + network: self.network.clone(), + chain: request.chain, + genesis_hash, + }) + .ok_or(RemoteChainInfoError::NotSupported) + } +} + +fn refuse_reserved_genesis(hash: [u8; 32]) -> Result<(), StackError> { + let hex_hash = hex::encode(hash); + match RESERVED_GENESIS_HASHES.contains(&hex_hash.as_str()) { + true => Err(StackError::Reserved { value: format!("genesis hash 0x{hex_hash}") }), + false => Ok(()), + } +} + +/// A genesis hash from a descriptor, which `trinity-stack` has already checked +/// is 32 bytes of 0x-prefixed lowercase hex. +fn genesis(hex_hash: &str) -> [u8; 32] { + let mut hash = [0; 32]; + hex::decode_to_slice(hex_hash.trim_start_matches("0x"), &mut hash) + .expect("trinity-stack validated genesisHash as 32 bytes of hex"); + hash +} + +#[cfg(test)] +mod tests; diff --git a/rust/crates/truapi/src/stack/tests.rs b/rust/crates/truapi/src/stack/tests.rs new file mode 100644 index 0000000000..23abc82bec --- /dev/null +++ b/rust/crates/truapi/src/stack/tests.rs @@ -0,0 +1,158 @@ +use super::*; +use crate::test_support::{sign_test_descriptor, test_descriptor, test_descriptor_chain}; + +const POLKADOT_PEOPLE: [u8; 32] = [ + 0x67, 0xfa, 0x17, 0x7a, 0x09, 0x7b, 0xfa, 0x18, 0xf7, 0x7e, 0xa9, 0x5a, 0xb5, 0x6e, 0x9b, 0xcd, + 0xfe, 0xb0, 0xe5, 0xb8, 0xa4, 0x0e, 0x46, 0x29, 0x8b, 0xb9, 0x3e, 0x16, 0xb6, 0xfc, 0x50, 0x08, +]; + +fn bundled( + descriptor: &serde_json::Value, + allows_reserved_values: bool, +) -> Result { + let (pointer, bytes) = sign_test_descriptor(descriptor); + Stack::bundled(&pointer, &bytes, allows_reserved_values) +} + +fn info( + stack: &Stack, + chain: ChainIdentifier, +) -> Result { + stack.chain_info(&RemoteChainInfoRequest { chain }) +} + +/// `get_chain_info` answers from the descriptor, so every host reports the +/// same chains and network for a stack. +#[test] +fn a_bundled_stack_answers_get_chain_info_from_its_descriptor() { + let stack = bundled(&test_descriptor("paseo", [0xa1; 32], [0xb2; 32], None), false) + .expect("stack is valid"); + + let response = |chain, genesis_hash| { + Ok(RemoteChainInfoResponse { network: "paseo".to_string(), chain, genesis_hash }) + }; + assert_eq!( + [ + ChainIdentifier::Relay, + ChainIdentifier::People, + ChainIdentifier::Bulletin, + ChainIdentifier::AssetHub + ] + .map(|chain| info(&stack, chain)), + [ + response(ChainIdentifier::Relay, [0x01; 32]), + response(ChainIdentifier::People, [0xa1; 32]), + response(ChainIdentifier::Bulletin, [0xb2; 32]), + Err(RemoteChainInfoError::NotSupported), + ] + ); +} + +/// A host refreshes its stack unseen, so a pointer with no key to check a +/// refresh against is refused even when the bundled copy is signed. +#[test] +fn a_pointer_without_a_key_is_refused() { + let (_, bytes) = sign_test_descriptor(&test_descriptor("paseo", [0xa1; 32], [0xb2; 32], None)); + let pointer = "polkadotapp://stack?url=https%3A%2F%2Fstack.test.invalid%2Fstack.json"; + + assert_eq!(Stack::bundled(pointer, &bytes, false), Err(StackError::Unsigned)); +} + +/// The pointer's key is the trust anchor: a copy signed by anyone else does +/// not start, whatever it claims. +#[test] +fn a_copy_signed_by_another_key_is_refused() { + let (_, bytes) = sign_test_descriptor(&test_descriptor("paseo", [0xa1; 32], [0xb2; 32], None)); + let other_key = format!("0x{}", hex::encode(trinity_stack::public_key(&[0x42; 32]))); + let pointer = Pointer::new("https://stack.test.invalid/stack.json", Some(&other_key)) + .expect("pointer is valid") + .to_link(trinity_stack::DEFAULT_LINK_BASE); + + let refused = Stack::bundled(&pointer, &bytes, false).expect_err("another key's copy"); + assert!( + matches!(&refused, StackError::Descriptor(error) if error.code() == "bad-signature"), + "{refused:?}" + ); +} + +/// SSO runs over People and in-core preimages go to Bulletin, so a stack +/// without either cannot start a host. +#[test] +fn a_stack_without_people_or_bulletin_is_refused() { + let mut descriptor = test_descriptor("paseo", [0xa1; 32], [0xb2; 32], None); + descriptor["chains"] = serde_json::json!([ + test_descriptor_chain("Relay", [0x01; 32], None), + test_descriptor_chain("People", [0xa1; 32], Some(1004)) + ]); + + assert_eq!(bundled(&descriptor, false), Err(StackError::MissingChain { role: "Bulletin" })); +} + +/// Products are named under the TLDs the core serves dotNS for. Until that +/// list comes from the stack, a stack on any other TLD cannot host products. +#[test] +fn a_tld_the_core_does_not_serve_is_refused() { + let chains = + || StackChains { relay: None, asset_hub: None, people: [0xa1; 32], bulletin: [0xb2; 32] }; + + assert_eq!( + ["kusama", "", ".paseo", "Paseo"].map(|tld| Stack::new( + "paseo".into(), + tld.into(), + chains(), + true + ) + .err()), + ["kusama", "", ".paseo", "Paseo"] + .map(|tld| Some(StackError::UnsupportedTld { tld: tld.to_string() })), + ); + assert_eq!( + bundled(&test_descriptor("kusama", [0xa1; 32], [0xb2; 32], None), false), + Err(StackError::UnsupportedTld { tld: "kusama".to_string() }) + ); +} + +/// Only a build that vouches for a stack may give it Polkadot's names: the +/// `dot` TLD, or a Polkadot genesis hash under any role, including ones the +/// core never reads. +#[test] +fn reserved_values_need_the_builds_permission() { + let on_dot = test_descriptor("dot", [0xa1; 32], [0xb2; 32], None); + let mut polkadot_people_as_another_role = + test_descriptor("paseo", [0xa1; 32], [0xb2; 32], None); + polkadot_people_as_another_role["chains"] + .as_array_mut() + .expect("chains is an array") + .push(test_descriptor_chain("Collectives", POLKADOT_PEOPLE, Some(1001))); + let polkadot_people = format!("genesis hash 0x{}", hex::encode(POLKADOT_PEOPLE)); + + assert_eq!( + [&on_dot, &polkadot_people_as_another_role].map(|descriptor| { + (bundled(descriptor, false).err(), bundled(descriptor, true).is_ok()) + }), + [ + (Some(StackError::Reserved { value: "tld dot".to_string() }), true), + (Some(StackError::Reserved { value: polkadot_people }), true), + ] + ); +} + +/// A stack a Rust host resolves itself meets the same reserved-value rule. +#[test] +fn a_stack_from_parts_is_held_to_the_reserved_values() { + let chains = + StackChains { relay: None, asset_hub: None, people: POLKADOT_PEOPLE, bulletin: [0xb2; 32] }; + + assert_eq!( + ( + Stack::new("polkadot".into(), "paseo".into(), chains.clone(), false).err(), + Stack::new("polkadot".into(), "paseo".into(), chains, true).is_ok(), + ), + ( + Some(StackError::Reserved { + value: format!("genesis hash 0x{}", hex::encode(POLKADOT_PEOPLE)) + }), + true, + ) + ); +} diff --git a/rust/crates/truapi/src/test_support.rs b/rust/crates/truapi/src/test_support.rs index b0c1c91aec..1492d2dc84 100644 --- a/rust/crates/truapi/src/test_support.rs +++ b/rust/crates/truapi/src/test_support.rs @@ -343,6 +343,77 @@ pub fn stub_platform() -> Arc { Arc::new(StubPlatform::default()) } +/// A stack on `tld` over these chains, without a relay. Reserved values are +/// allowed, so a test may run on `dot`. +pub fn test_stack( + tld: &str, + people: [u8; 32], + bulletin: [u8; 32], + asset_hub: Option<[u8; 32]>, +) -> crate::platform::Stack { + let chains = crate::platform::StackChains { relay: None, asset_hub, people, bulletin }; + crate::platform::Stack::new("paseo".to_string(), tld.to_string(), chains, true) + .expect("test stack is valid") +} + +/// The key [`sign_test_descriptor`] signs with. +pub const TEST_STACK_SECRET: [u8; 32] = [0x5e; 32]; + +/// A descriptor for a stack on `tld` over a relay and these chains, ready for +/// [`sign_test_descriptor`]. +pub fn test_descriptor( + tld: &str, + people: [u8; 32], + bulletin: [u8; 32], + asset_hub: Option<[u8; 32]>, +) -> serde_json::Value { + let mut chains = vec![ + test_descriptor_chain("Relay", [0x01; 32], None), + test_descriptor_chain("People", people, Some(1004)), + test_descriptor_chain("Bulletin", bulletin, Some(1010)), + ]; + chains.extend(asset_hub.map(|genesis| test_descriptor_chain("AssetHub", genesis, Some(1000)))); + serde_json::json!({ + "$v": 1, + "sequence": 1, + "generation": "test", + "id": "test", + "name": "Test", + "network": "paseo", + "tld": tld, + "chains": chains, + "services": [], + }) +} + +/// One `chains` entry of a test descriptor. +pub fn test_descriptor_chain( + role: &str, + genesis: [u8; 32], + para_id: Option, +) -> serde_json::Value { + let mut chain = serde_json::json!({ + "role": role, + "genesisHash": format!("0x{}", hex::encode(genesis)), + "rpc": ["wss://rpc.test.invalid"], + }); + if let Some(para_id) = para_id { + chain["paraId"] = para_id.into(); + } + chain +} + +/// `descriptor` signed with [`TEST_STACK_SECRET`], and the pointer link that +/// verifies it. +pub fn sign_test_descriptor(descriptor: &serde_json::Value) -> (String, Vec) { + let bytes = serde_json::to_vec(descriptor).expect("a JSON value serialises"); + let signed = trinity_stack::sign(&bytes, &TEST_STACK_SECRET).expect("test descriptor is valid"); + let key = format!("0x{}", hex::encode(trinity_stack::public_key(&TEST_STACK_SECRET))); + let pointer = trinity_stack::Pointer::new("https://stack.test.invalid/stack.json", Some(&key)) + .expect("test pointer is valid"); + (pointer.to_link(trinity_stack::DEFAULT_LINK_BASE), signed) +} + /// Runtime configuration used by platform-backed runtime tests. pub fn runtime_config(product_id: &str) -> (PairingHostConfig, ProductContext) { ( @@ -354,9 +425,7 @@ pub fn runtime_config(product_id: &str) -> (PairingHostConfig, ProductContext) { platform: truapi::latest::HostPlatform::Web, }, PlatformInfo::default(), - [0; 32], - [0xbb; 32], - [0xcc; 32], + crate::test_support::test_stack("paseo", [0; 32], [0xbb; 32], Some([0xcc; 32])), "polkadotapp".to_string(), ) .expect("test host runtime config is valid"), @@ -409,9 +478,7 @@ pub fn sso_host_and_responder_sessions() -> (SsoSessionInfo, SsoSessionInfo) { platform: truapi::latest::HostPlatform::Unknown, }, PlatformInfo::default(), - [0; 32], - [0xbb; 32], - [0xcc; 32], + crate::test_support::test_stack("paseo", [0; 32], [0xbb; 32], Some([0xcc; 32])), "polkadotapp".to_string(), ) .expect("test pairing config is valid"); @@ -1271,22 +1338,6 @@ impl PlatformFeatures for StubPlatform { ) -> Result { Ok(v01::HostFeatureSupportedResponse { supported: true }) } - - async fn supported_chains(&self) -> Result { - Ok(crate::platform::HostChainSet { - network: "paseo".to_string(), - chains: vec![ - crate::platform::HostChainEntry { - identifier: v01::ChainIdentifier::AssetHub, - genesis_hash: [0xaa; 32], - }, - crate::platform::HostChainEntry { - identifier: v01::ChainIdentifier::People, - genesis_hash: [0x22; 32], - }, - ], - }) - } } struct RecordingConnection { diff --git a/rust/crates/truapi/src/truapi_core.rs b/rust/crates/truapi/src/truapi_core.rs index 86356537cc..916e362428 100644 --- a/rust/crates/truapi/src/truapi_core.rs +++ b/rust/crates/truapi/src/truapi_core.rs @@ -59,9 +59,7 @@ impl TrUApiCore { let services = RuntimeServices::new( platform, host_config.host.host_info.clone(), - host_config.people_chain_genesis_hash, - host_config.bulletin_chain_genesis_hash, - host_config.asset_hub_chain_genesis_hash, + host_config.stack.clone(), spawner.clone(), ); let pairing_host = PairingHostRole::new(services.clone(), host_config); diff --git a/rust/crates/truapi/src/wasm.rs b/rust/crates/truapi/src/wasm.rs index 9f2d68d832..293fb27187 100644 --- a/rust/crates/truapi/src/wasm.rs +++ b/rust/crates/truapi/src/wasm.rs @@ -24,11 +24,13 @@ use std::{ #[cfg(feature = "wasm-signing-host")] use crate::platform::SigningHostConfig; +#[cfg(feature = "test-host")] +use crate::platform::StackChains; use crate::platform::{ ChainProvider, ChatPlatform, ContactsPlatform, ExecutableKind, GamePlatform, HostInfo, JsonRpcConnection, PairingHostConfig, PermissionStatusHost, PlatformInfo, PocketPlatform, ProductContext, ProductId, ProductIdError, ProviderError, RuntimeConfigValidationError, - ScannerPlatform, + ScannerPlatform, Stack, }; use futures::{ channel::mpsc, @@ -494,9 +496,6 @@ fn pairing_host_config_from_js(value: &JsValue) -> Result Result Result Result Result { + #[cfg(feature = "test-host")] + if let Some(test_stack) = get_optional_object(value, "testStack", "runtimeConfig.testStack")? { + return test_stack_from_js(&test_stack); + } + let stack = get_required_object(value, "stack", "runtimeConfig.stack")?; + let pointer = get_required_string_at(&stack, "pointer", "runtimeConfig.stack.pointer")?; + let descriptor = Reflect::get(&stack, &JsValue::from_str("descriptor"))? + .dyn_into::() + .map_err(|_| JsValue::from_str("runtimeConfig.stack.descriptor must be a Uint8Array"))? + .to_vec(); + let allows_reserved_values = get_optional_bool_at( + &stack, + "allowsReservedValues", + "runtimeConfig.stack.allowsReservedValues", + )? + .unwrap_or(false); + Stack::bundled(&pointer, &descriptor, allows_reserved_values) + .map_err(|error| JsValue::from_str(&format!("runtimeConfig.stack: {error}"))) +} + +/// `runtimeConfig.testStack`: `network`, `tld`, `people`, `bulletin` and the +/// optional `assetHub` and `relay`, each `{ genesisHash }`, and +/// `allowsReservedValues`, which defaults to false. +#[cfg(feature = "test-host")] +fn test_stack_from_js(stack: &JsValue) -> Result { + let genesis = |role: &str| { + let path = format!("runtimeConfig.testStack.{role}"); + get_optional_object(stack, role, &path)? + .map(|chain| { + get_required_bytes32_at(&chain, "genesisHash", &format!("{path}.genesisHash")) + }) + .transpose() + }; + let required = |role: &str| { + genesis(role)?.ok_or_else(|| { + JsValue::from_str(&format!("runtimeConfig.testStack.{role} is required")) + }) + }; + let chains = StackChains { + relay: genesis("relay")?, + asset_hub: genesis("assetHub")?, + people: required("people")?, + bulletin: required("bulletin")?, + }; + Stack::new( + get_required_string_at(stack, "network", "runtimeConfig.testStack.network")?, + get_required_string_at(stack, "tld", "runtimeConfig.testStack.tld")?, + chains, + get_optional_bool_at( + stack, + "allowsReservedValues", + "runtimeConfig.testStack.allowsReservedValues", + )? + .unwrap_or(false), + ) + .map_err(|error| JsValue::from_str(&format!("runtimeConfig.testStack: {error}"))) +} + fn product_context_from_js(value: &JsValue) -> Result { if value.is_null() || value.is_undefined() { return Err(JsValue::from_str("product is required")); @@ -620,10 +670,6 @@ fn runtime_config_field_to_js(field: &str) -> &str { match field { "host_info.name" => "host.name", "pairing_deeplink_scheme" => "pairing.deeplinkScheme", - "people_chain_genesis_hash" => "people.genesisHash", - "bulletin_chain_genesis_hash" => "bulletin.genesisHash", - "asset_hub_chain_genesis_hash" => "assetHub.genesisHash", - "network_suffix" => "networkSuffix", other => other, } } @@ -659,11 +705,6 @@ fn runtime_config_validation_to_js(err: RuntimeConfigValidationError) -> JsValue }) => JsValue::from_str(&format!( "runtimeConfig.productId must be at most {limit} bytes, got {actual}" )), - RuntimeConfigValidationError::InvalidNetworkSuffix { network_suffix } => { - JsValue::from_str(&format!( - "runtimeConfig.networkSuffix must be a supported dotNS TLD, got {network_suffix:?}" - )) - }, } } @@ -708,6 +749,17 @@ fn get_optional_string_at( .ok_or_else(|| JsValue::from_str(&format!("{path} must be a string"))) } +fn get_optional_bool_at(value: &JsValue, name: &str, path: &str) -> Result, JsValue> { + let property = Reflect::get(value, &JsValue::from_str(name))?; + if property.is_null() || property.is_undefined() { + return Ok(None); + } + property + .as_bool() + .map(Some) + .ok_or_else(|| JsValue::from_str(&format!("{path} must be a boolean"))) +} + fn get_required_string_at(value: &JsValue, name: &str, path: &str) -> Result { get_optional_string_at(value, name, path)? .ok_or_else(|| JsValue::from_str(&format!("{path} is required"))) diff --git a/rust/crates/truapi/tests/common/mod.rs b/rust/crates/truapi/tests/common/mod.rs index 89a6d33005..201b332d47 100644 --- a/rust/crates/truapi/tests/common/mod.rs +++ b/rust/crates/truapi/tests/common/mod.rs @@ -24,9 +24,7 @@ pub fn test_runtime_config() -> (PairingHostConfig, ProductContext) { platform: truapi::latest::HostPlatform::Web, }, PlatformInfo::default(), - [0xa2; 32], - [0xbb; 32], - [0xcc; 32], + truapi::platform::mock::mock_stack([0xa2; 32], [0xbb; 32], Some([0xcc; 32])), "polkadotapp".to_string(), ) .expect("test host runtime config is valid"), @@ -128,16 +126,6 @@ impl Features for WireShapePlatform { ) -> Result { Ok(v01::HostFeatureSupportedResponse { supported: true }) } - - async fn supported_chains(&self) -> Result { - Ok(truapi::platform::HostChainSet { - network: "paseo".to_string(), - chains: vec![truapi::platform::HostChainEntry { - identifier: v01::ChainIdentifier::AssetHub, - genesis_hash: [0xaa; 32], - }], - }) - } } struct DeadConnection; diff --git a/rust/crates/truapi/tests/platform_bounds.rs b/rust/crates/truapi/tests/platform_bounds.rs index cb4681aa88..ff7fe53dda 100644 --- a/rust/crates/truapi/tests/platform_bounds.rs +++ b/rust/crates/truapi/tests/platform_bounds.rs @@ -95,9 +95,7 @@ fn pairing_config_validation_cases() { platform: truapi::latest::HostPlatform::Web, }, PlatformInfo::default(), - [0xa2; 32], - [0xbb; 32], - [0xcc; 32], + truapi::platform::mock::mock_stack([0xa2; 32], [0xbb; 32], Some([0xcc; 32])), case.pairing_deeplink_scheme.to_string(), ) .map(|_| ()); diff --git a/rust/crates/truapi/tests/signing_host_mock_platform.rs b/rust/crates/truapi/tests/signing_host_mock_platform.rs index ccb2abc215..19d5f11d44 100644 --- a/rust/crates/truapi/tests/signing_host_mock_platform.rs +++ b/rust/crates/truapi/tests/signing_host_mock_platform.rs @@ -97,11 +97,7 @@ fn signing_config() -> SigningHostConfig { platform: truapi::latest::HostPlatform::Unknown, }, PlatformInfo::default(), - [0; 32], - [0xbb; 32], - // No dotNS Asset Hub, so activation looks up no username. - [0; 32], - "paseo".to_string(), + truapi::platform::mock::mock_stack([0; 32], [0xbb; 32], None), ) .expect("signing host config is valid") } diff --git a/rust/crates/truapi/tests/wasm_crypto_vectors.rs b/rust/crates/truapi/tests/wasm_crypto_vectors.rs index a670302349..a5d646d899 100644 --- a/rust/crates/truapi/tests/wasm_crypto_vectors.rs +++ b/rust/crates/truapi/tests/wasm_crypto_vectors.rs @@ -52,9 +52,7 @@ fn runtime_config() -> PairingHostConfig { platform: truapi::latest::HostPlatform::Web, }, PlatformInfo { kind: Some("Firefox".to_string()), version: Some("192.32".to_string()) }, - [0xa2; 32], - [0xbb; 32], - [0xcc; 32], + truapi::platform::mock::mock_stack([0xa2; 32], [0xbb; 32], Some([0xcc; 32])), "polkadotapp".to_string(), ) .expect("test runtime config is valid") diff --git a/rust/crates/truapi/tests/wire_result_shape.rs b/rust/crates/truapi/tests/wire_result_shape.rs index faa419c899..de860d26bf 100644 --- a/rust/crates/truapi/tests/wire_result_shape.rs +++ b/rust/crates/truapi/tests/wire_result_shape.rs @@ -147,9 +147,9 @@ fn get_chain_info_ok_response_round_trips_over_the_wire() { truapi::versioned::chain::RemoteChainInfoResponse, CallError, > = Ok(truapi::versioned::chain::RemoteChainInfoResponse::V1(v01::RemoteChainInfoResponse { - network: "paseo".to_string(), + network: "mock".to_string(), chain: v01::ChainIdentifier::AssetHub, - genesis_hash: [0xaa; 32], + genesis_hash: [0xcc; 32], })); assert_eq!(response.payload.value, expected.encode()); } @@ -157,7 +157,8 @@ fn get_chain_info_ok_response_round_trips_over_the_wire() { #[test] fn get_chain_info_unserved_chain_uses_err_discriminant() { let core = make_core(); - let request = v01::RemoteChainInfoRequest { chain: v01::ChainIdentifier::Bulletin }; + // The test stack has no relay. + let request = v01::RemoteChainInfoRequest { chain: v01::ChainIdentifier::Relay }; let ids = request_ids("chain_get_chain_info").expect("known request method"); let frame = ProtocolMessage { request_id: "p:10".into(), From fbd44e614545b07a4b66df701b49fda41e367906 Mon Sep 17 00:00:00 2001 From: Mikelle Date: Fri, 6 Nov 2026 17:57:44 +0100 Subject: [PATCH 2/3] fix(truapi): report the descriptor's network and drop the zero Asset Hub The CLI answers get_chain_info with the stack descriptor's network, as an app bundling the same copy does, and keeps its id only as the product namespace. It grants Polkadot's reserved values to the network whose identities are real instead of matching an id a descriptor controls. Session identity lookup takes the stack's Asset Hub as an Option, so the all-zero genesis no longer stands for "none". The signing role's test runtime has no Asset Hub, and the PGAS test that waits on Asset Hub configures one. startTestHost and createMockClient take Partial, so a suite can pass the testStack they read. StackError derives only what is used. --- .changeset/stack-chain-config.md | 5 +- .../src/testing/create-mock-client.ts | 4 +- .../truapi-host/src/testing/host-page.ts | 4 +- rust/crates/truapi-host-cli/README.md | 2 +- rust/crates/truapi-host-cli/SPEC.md | 8 +-- rust/crates/truapi-host-cli/src/network.rs | 29 +++++++---- .../truapi-host-cli/src/network/tests.rs | 50 +++++++++++++------ rust/crates/truapi/src/runtime/identity.rs | 17 +++---- .../truapi/src/runtime/identity/tests.rs | 8 ++- .../runtime/signing_host/local_activation.rs | 2 +- .../truapi/src/runtime/signing_host/tests.rs | 10 ++-- .../truapi/src/runtime/sso_request_service.rs | 6 +-- .../runtime/sso_request_service/pairing.rs | 2 +- rust/crates/truapi/src/stack.rs | 2 +- 14 files changed, 89 insertions(+), 60 deletions(-) diff --git a/.changeset/stack-chain-config.md b/.changeset/stack-chain-config.md index 6d0a607e91..a279e4a982 100644 --- a/.changeset/stack-chain-config.md +++ b/.changeset/stack-chain-config.md @@ -10,5 +10,6 @@ pointer without a key, a descriptor without a People or Bulletin chain, and a re `getChainInfo` answers from the descriptor's `chains` and `network`, so the `supportedChains` host callback and the `HostChainSet` and `HostChainEntry` types are removed. The test host runs a stack built from its parts: `mockRuntimeConfig()` returns a `testStack` over the `MOCK_GENESIS` chains, which only the `wasm/testing` bundle reads, -`fromNetworks` and `liveChain` build it from their chains, and `MockHostConfig.servedChains` replaces `supportedChains` -as the genesis hashes `featureSupported` answers from under `onlyServedChains`. +`startTestHost` and `createMockClient` take `Partial` overrides, `fromNetworks` and `liveChain` build +it from their chains, and `MockHostConfig.servedChains` replaces `supportedChains` as the genesis hashes +`featureSupported` answers from under `onlyServedChains`. diff --git a/js/packages/truapi-host/src/testing/create-mock-client.ts b/js/packages/truapi-host/src/testing/create-mock-client.ts index dc313a6299..8fe8077305 100644 --- a/js/packages/truapi-host/src/testing/create-mock-client.ts +++ b/js/packages/truapi-host/src/testing/create-mock-client.ts @@ -28,8 +28,8 @@ import { mockRuntimeConfig, type MockHost, type MockHostConfig, + type MockRuntimeConfig, } from "../web/create-mock-host.js"; -import type { ProductRuntimeConfig } from "../runtime.js"; import { resolveAccount, type DevAccount, @@ -41,7 +41,7 @@ export interface MockClientOptions { /** Behaviour knobs forwarded to {@link createMockHost}. */ mock?: MockHostConfig; /** Overrides merged into {@link mockRuntimeConfig}. */ - runtimeConfig?: Partial; + runtimeConfig?: Partial; /** Account the host signs as. Defaults to `"alice"`. */ account?: DevAccountName | DevAccount; /** diff --git a/js/packages/truapi-host/src/testing/host-page.ts b/js/packages/truapi-host/src/testing/host-page.ts index aef7ac8aa8..7324887ec6 100644 --- a/js/packages/truapi-host/src/testing/host-page.ts +++ b/js/packages/truapi-host/src/testing/host-page.ts @@ -33,8 +33,8 @@ import { mockRuntimeConfig, type MockHost, type MockHostConfig, + type MockRuntimeConfig, } from "../web/create-mock-host.js"; -import type { ProductRuntimeConfig } from "../runtime.js"; import { checkDerivationIndex, resolveAccount, @@ -59,7 +59,7 @@ export interface TestHostPageOptions { /** Behaviour knobs forwarded to {@link createMockHost}. */ mock?: MockHostConfig; /** Overrides merged into {@link mockRuntimeConfig}. */ - runtimeConfig?: Partial; + runtimeConfig?: Partial; /** * Resolved WASM entry. Defaults to the signing-enabled `testing` bundle, * which is what lets the host own dev accounts instead of waiting on a diff --git a/rust/crates/truapi-host-cli/README.md b/rust/crates/truapi-host-cli/README.md index 5148633a50..b94606f9f9 100644 --- a/rust/crates/truapi-host-cli/README.md +++ b/rust/crates/truapi-host-cli/README.md @@ -1112,7 +1112,7 @@ role), its `dub` service as the identity backend (the `dub` URL with `/api/v1` appended), its `tld` and its `dotns.popController` replace the network's. A stack that lacks one of those roles, gives two of them one genesis hash, has a `tld` the core does not serve (`paseo`, `testnet`), is unreachable or is past -`expiresAt` is an error. The product namespace (the network `get_chain_info` reports and the +`expiresAt` is an error. `get_chain_info` reports the descriptor's `network`. The product namespace (the state directory) is the descriptor's `id` without a trailing `-fork`. Use a separate `--base-path` and a disposable identity for a local stack. The routing applies to every chain role the host serves, including the Bulletin node the CLI diff --git a/rust/crates/truapi-host-cli/SPEC.md b/rust/crates/truapi-host-cli/SPEC.md index c0b1853003..1b74431c19 100644 --- a/rust/crates/truapi-host-cli/SPEC.md +++ b/rust/crates/truapi-host-cli/SPEC.md @@ -1757,7 +1757,7 @@ descriptor that breaks a rule and one past `expiresAt` are errors that name the stack; one past `validUntil` is used with a warning. Its endpoints and People's `NetworkSuffix` are checked as above. `--chain-backend light-client` is refused with `--stack`, since the light client syncs only the networks -`truapi-provider` bundles. The product namespace (the network `get_chain_info` reports and the +`truapi-provider` bundles. `get_chain_info` reports the descriptor's `network`. The product namespace (the state directory) is the descriptor's `id` without a trailing `-fork`, which must be a lowercase label. @@ -2028,9 +2028,9 @@ Deliberately unavailable methods: - all four Payment methods, which answer typed `Unknown` domain errors. A successful `System/feature_supported` call resolves the queried chain against -the host's chain set, the same set `Chain/get_chain_info` answers from, so it -returns `true` for the network's People, Bulletin and Asset Hub genesis hashes and -`false` for anything else. Success means the method is wired, not that every feature +the chains the host serves, the People, Bulletin and Asset Hub of its stack, the +same roles `Chain/get_chain_info` answers for, so it returns `true` for those +genesis hashes and `false` for anything else. Success means the method is wired, not that every feature is present. ### 15.2 Platform-specific semantics diff --git a/rust/crates/truapi-host-cli/src/network.rs b/rust/crates/truapi-host-cli/src/network.rs index 3b61ad2ce3..ff410948ba 100644 --- a/rust/crates/truapi-host-cli/src/network.rs +++ b/rust/crates/truapi-host-cli/src/network.rs @@ -35,8 +35,7 @@ pub enum Network { PaseoNextV2, /// Built-in stack, read from the descriptor its operator publishes. Its id is /// the id the playground and dotli already use for this network - /// (`VITE_NETWORKS=paseo-next-v2,previewnet`), so the network `get_chain_info` - /// reads the same string a product sees everywhere else. + /// (`VITE_NETWORKS=paseo-next-v2,previewnet`). #[value(name = "previewnet")] Previewnet, /// Polkadot mainnet (People, Bulletin, Asset Hub). Identities are real: @@ -610,6 +609,7 @@ fn stack_config(descriptor: &Descriptor, id: &'static str) -> Result`, `peopl.`), so a person the @@ -1009,11 +1013,14 @@ impl NetworkConfig { ] } - /// The stack the core runs on: [`Self::served_roles`], the network `id` - /// `get_chain_info` reports, and the TLD. The CLI resolved and checked these - /// values itself, from a descriptor, the local-network file or Polkadot's - /// pinned values, so it hands them to the core as parts. Only Polkadot may - /// use Polkadot's reserved values. + /// The stack the core runs on: [`Self::served_roles`], the + /// [`Self::network`] `get_chain_info` reports, and the TLD. The CLI resolved + /// and checked these values itself, from a descriptor, the local-network + /// file or Polkadot's pinned values, so it hands them to the core as parts. + /// + /// Only a network whose identities are real may use Polkadot's reserved + /// values. Every stack read from a descriptor has disposable identities, + /// so its id cannot grant them. pub fn stack(&self) -> Result { let chains = StackChains { relay: None, @@ -1022,10 +1029,10 @@ impl NetworkConfig { bulletin: self.bulletin_genesis, }; Stack::new( - self.id.to_string(), + self.network.to_string(), self.network_suffix.to_string(), chains, - self.id == POLKADOT.id, + !self.disposable_identities, ) } diff --git a/rust/crates/truapi-host-cli/src/network/tests.rs b/rust/crates/truapi-host-cli/src/network/tests.rs index 07d4419284..ccd4de1ba2 100644 --- a/rust/crates/truapi-host-cli/src/network/tests.rs +++ b/rust/crates/truapi-host-cli/src/network/tests.rs @@ -961,25 +961,43 @@ fn test_refresh_expired_error() { assert!(refresh(&sign_copy(10, "g1"), Fetched::Document(&published)).is_err()); } -/// The core answers `get_chain_info` from the stack the CLI builds, so the -/// stack carries the network id products see and each served role's genesis. +/// A product sees the same network, TLD and chains from the CLI as from an app +/// bundling the same stack: the descriptor's `network`, not the CLI's product +/// namespace for it. The CLI serves no Relay, so it names none. +#[test] +fn the_stack_reports_what_an_app_bundling_it_reports() { + let stacks = |network: Network| { + let built_in = network.built_in_stack()?; + let bundled = Stack::bundled(built_in.link.trim(), built_in.bundle, false) + .expect("the bundled copy starts an app"); + let cli = network.config().stack().expect("the CLI's stack starts"); + Some((network, cli, bundled)) + }; + + for (network, cli, bundled) in Network::value_variants().iter().copied().filter_map(stacks) { + let expected = Stack::new( + bundled.network().to_string(), + bundled.tld().to_string(), + StackChains { relay: None, ..bundled.chains().clone() }, + false, + ); + assert_eq!(Ok(cli), expected, "{}", network.id()); + } +} + +/// Only Polkadot's pinned values may use Polkadot's genesis hashes and `dot`. +/// A stack read from a descriptor has disposable identities whatever its id +/// says, so the core holds it to the reserved values. #[test] -fn the_stack_names_the_configured_network() { - let config = Network::default().config(); - let stack = config.stack().expect("the default network is a valid stack"); +fn a_descriptor_stack_cannot_claim_polkadot_reserved_values() { + let dot = edit_stack(|descriptor| { + descriptor["id"] = "polkadot-fork".into(); + descriptor["tld"] = "dot".into(); + }); assert_eq!( - (stack.network(), stack.tld(), stack.chains().clone()), - ( - config.id, - config.network_suffix, - truapi::platform::StackChains { - relay: None, - asset_hub: Some(config.asset_hub_genesis), - people: config.people_genesis, - bulletin: config.bulletin_genesis, - } - ), + stack_network_config(&dot).unwrap().stack(), + Err(StackError::Reserved { value: "tld dot".to_string() }) ); } diff --git a/rust/crates/truapi/src/runtime/identity.rs b/rust/crates/truapi/src/runtime/identity.rs index 9fb744b266..42ceede79c 100644 --- a/rust/crates/truapi/src/runtime/identity.rs +++ b/rust/crates/truapi/src/runtime/identity.rs @@ -36,11 +36,12 @@ const LOOKUP_BUDGET: Duration = Duration::from_secs(45); /// Fills in missing usernames by querying the dotNS contracts on Asset Hub. /// Returns the session unchanged when it already carries a username. Also -/// returns it unchanged when no Asset Hub is configured. +/// returns it unchanged when `asset_hub_chain_genesis_hash` is `None`, for a +/// stack that names no Asset Hub. #[instrument(skip_all, fields(runtime.method = "session.identity.resolve_with_chain"))] pub async fn resolve_session_identity_with_chain( chain: &ChainRuntime, - asset_hub_chain_genesis_hash: [u8; 32], + asset_hub_chain_genesis_hash: Option<[u8; 32]>, session: SessionInfo, ) -> SessionInfo { if session.has_username() { @@ -57,18 +58,16 @@ pub async fn resolve_session_identity_with_chain( /// Reads `session`'s usernames from the dotNS contracts on Asset Hub, whatever /// usernames it already carries. The identity account is tried first, then the -/// root key when the identity account holds no username. Returns `None` when no -/// Asset Hub is configured, neither account holds a username, or the lookup -/// fails. +/// root key when the identity account holds no username. Returns `None` when +/// `asset_hub_chain_genesis_hash` is `None`, neither account holds a username, +/// or the lookup fails. #[instrument(skip_all, fields(runtime.method = "session.identity.lookup_session"))] pub async fn lookup_session_identity( chain: &ChainRuntime, - asset_hub_chain_genesis_hash: [u8; 32], + asset_hub_chain_genesis_hash: Option<[u8; 32]>, session: &SessionInfo, ) -> Option { - if asset_hub_chain_genesis_hash == [0; 32] { - return None; - } + let asset_hub_chain_genesis_hash = asset_hub_chain_genesis_hash?; let budget = futures_timer::Delay::new(LOOKUP_BUDGET).fuse(); pin_mut!(budget); diff --git a/rust/crates/truapi/src/runtime/identity/tests.rs b/rust/crates/truapi/src/runtime/identity/tests.rs index 5c480be826..f68d6575c6 100644 --- a/rust/crates/truapi/src/runtime/identity/tests.rs +++ b/rust/crates/truapi/src/runtime/identity/tests.rs @@ -280,7 +280,9 @@ fn in_core_lookup_resolves_usernames_over_one_runtime_follow() { let session = session_without_usernames([0x11; 32], ACCOUNT); let resolved = futures::executor::block_on(resolve_session_identity_with_chain( - &chain, [0xcc; 32], session, + &chain, + Some([0xcc; 32]), + session, )); // The pending claim yields the lite name; the settled store yields the @@ -319,7 +321,9 @@ fn root_key_resolves_when_identity_account_labels_are_unissued() { let session = session_without_usernames(ACCOUNT, SQUATTER); let resolved = futures::executor::block_on(resolve_session_identity_with_chain( - &chain, [0xcc; 32], session, + &chain, + Some([0xcc; 32]), + session, )); assert_eq!( diff --git a/rust/crates/truapi/src/runtime/signing_host/local_activation.rs b/rust/crates/truapi/src/runtime/signing_host/local_activation.rs index 955e8f16b8..7e51fee642 100644 --- a/rust/crates/truapi/src/runtime/signing_host/local_activation.rs +++ b/rust/crates/truapi/src/runtime/signing_host/local_activation.rs @@ -66,7 +66,7 @@ impl LocalActivation for SigningHost { (self.services.spawner)(Box::pin(async move { let Some(identity) = lookup_session_identity( &services.chain, - services.asset_hub_chain_genesis_hash().unwrap_or_default(), + services.asset_hub_chain_genesis_hash(), &session, ) .await diff --git a/rust/crates/truapi/src/runtime/signing_host/tests.rs b/rust/crates/truapi/src/runtime/signing_host/tests.rs index 56609accbd..56ef69732e 100644 --- a/rust/crates/truapi/src/runtime/signing_host/tests.rs +++ b/rust/crates/truapi/src/runtime/signing_host/tests.rs @@ -105,7 +105,7 @@ impl RingResolver for StubRingResolver { #[test] fn the_signing_role_adjudicates_grants_against_the_asset_hub_it_installed() { let (services, _authority) = - signing_runtime_on_asset_hub(Arc::new(StubPlatform::default()), [0xcc; 32]); + signing_runtime_on_asset_hub(Arc::new(StubPlatform::default()), Some([0xcc; 32])); assert_eq!( services.asset_hub_chain_genesis_hash(), Some([0xcc; 32]), @@ -145,12 +145,12 @@ fn signing_runtime() -> (Arc, Arc) { fn signing_runtime_with_platform( platform: Arc, ) -> (Arc, Arc) { - signing_runtime_on_asset_hub(platform, [0; 32]) + signing_runtime_on_asset_hub(platform, None) } fn signing_runtime_on_asset_hub( platform: Arc, - asset_hub_chain_genesis_hash: [u8; 32], + asset_hub_chain_genesis_hash: Option<[u8; 32]>, ) -> (Arc, Arc) { let config = SigningHostConfig::new( HostInfo { @@ -164,7 +164,7 @@ fn signing_runtime_on_asset_hub( TEST_NETWORK_SUFFIX, [0; 32], [0xbb; 32], - Some(asset_hub_chain_genesis_hash), + asset_hub_chain_genesis_hash, ), ) .expect("signing host config is valid"); @@ -2602,7 +2602,7 @@ fn a_pgas_request_waits_on_the_asset_hub_connection() { let platform = Arc::new(StubPlatform { chain_connect_pending: true, ..StubPlatform::default() }); - let (_services, authority) = signing_runtime_with_platform(platform); + let (_services, authority) = signing_runtime_on_asset_hub(platform, Some([0xcc; 32])); futures::executor::block_on(authority.activate_local_session(ENTROPY.to_vec())) .expect("activation"); let session = authority.current_session().expect("connected"); diff --git a/rust/crates/truapi/src/runtime/sso_request_service.rs b/rust/crates/truapi/src/runtime/sso_request_service.rs index 12806e2ad2..2978e462f2 100644 --- a/rust/crates/truapi/src/runtime/sso_request_service.rs +++ b/rust/crates/truapi/src/runtime/sso_request_service.rs @@ -299,7 +299,7 @@ impl SsoRequestService { let activation_epoch = self.advance_session_lifecycle(); let resolved = resolve_session_identity_with_chain( &self.chain, - self.host_config.stack.chains().asset_hub.unwrap_or_default(), + self.host_config.stack.chains().asset_hub, session, ) .await; @@ -356,7 +356,7 @@ impl SsoRequestService { }; let resolved = resolve_session_identity_with_chain( &self.chain, - self.host_config.stack.chains().asset_hub.unwrap_or_default(), + self.host_config.stack.chains().asset_hub, session, ) .await; @@ -611,7 +611,7 @@ impl SsoRequestService { let resolved = resolve_session_identity_with_chain( &self.chain, - self.host_config.stack.chains().asset_hub.unwrap_or_default(), + self.host_config.stack.chains().asset_hub, current.clone(), ) .await; diff --git a/rust/crates/truapi/src/runtime/sso_request_service/pairing.rs b/rust/crates/truapi/src/runtime/sso_request_service/pairing.rs index a9f1545ea7..3aefc68818 100644 --- a/rust/crates/truapi/src/runtime/sso_request_service/pairing.rs +++ b/rust/crates/truapi/src/runtime/sso_request_service/pairing.rs @@ -259,7 +259,7 @@ impl<'a> SsoPairingFlow<'a> { }; let resolve_session = resolve_session_identity_with_chain( &self.host.chain, - self.host.host_config.stack.chains().asset_hub.unwrap_or_default(), + self.host.host_config.stack.chains().asset_hub, session, ) .fuse(); diff --git a/rust/crates/truapi/src/stack.rs b/rust/crates/truapi/src/stack.rs index 9af7c3d8fb..b8ba7ef073 100644 --- a/rust/crates/truapi/src/stack.rs +++ b/rust/crates/truapi/src/stack.rs @@ -51,7 +51,7 @@ pub struct StackChains { } /// Why a stack was refused. -#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display, derive_more::Error)] +#[derive(Debug, PartialEq, derive_more::Display, derive_more::Error)] pub enum StackError { /// The pointer is not a stack link. #[display("stack pointer is not a stack link: {_0}")] From a2c180ff4fe2e3a87f9843911586842c3d579c18 Mon Sep 17 00:00:00 2001 From: Mikelle Date: Fri, 6 Nov 2026 18:17:49 +0100 Subject: [PATCH 3/3] fix(truapi): compile the test stack's hex parsing only in test-host builds The production wasm bundle reads its stack from a signed descriptor, so the 32-byte hex helpers serve only runtimeConfig.testStack and were dead code, which -D warnings rejects. --- rust/crates/truapi/src/wasm.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rust/crates/truapi/src/wasm.rs b/rust/crates/truapi/src/wasm.rs index 293fb27187..b8a2ef7bb8 100644 --- a/rust/crates/truapi/src/wasm.rs +++ b/rust/crates/truapi/src/wasm.rs @@ -765,6 +765,7 @@ fn get_required_string_at(value: &JsValue, name: &str, path: &str) -> Result Result<[u8; 32], JsValue> { get_optional_bytes32_at(value, name, path)? .ok_or_else(|| JsValue::from_str(&format!("{path} is required"))) } +#[cfg(feature = "test-host")] fn parse_hex32(value: &str) -> Result<[u8; 32], String> { let raw = value.strip_prefix("0x").unwrap_or(value); if raw.len() != 64 {