diff --git a/.changeset/stack-chain-config.md b/.changeset/stack-chain-config.md new file mode 100644 index 0000000000..a279e4a982 --- /dev/null +++ b/.changeset/stack-chain-config.md @@ -0,0 +1,15 @@ +--- +"@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, +`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/.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 d8f86545b9..f49a4ae128 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/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/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/host-page.ts b/js/packages/truapi-host/src/testing/host-page.ts index 39cc81c17f..a8826d1aa3 100644 --- a/js/packages/truapi-host/src/testing/host-page.ts +++ b/js/packages/truapi-host/src/testing/host-page.ts @@ -37,8 +37,8 @@ import { mockRuntimeConfig, type MockHost, type MockHostConfig, + type MockRuntimeConfig, } from "../web/create-mock-host.js"; -import type { ProductRuntimeConfig } from "../runtime.js"; import { checkDerivationIndex, resolveAccount, @@ -63,7 +63,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/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 d1ab840430..ae8371596e 100644 --- a/js/packages/truapi-host/src/testing/playwright.ts +++ b/js/packages/truapi-host/src/testing/playwright.ts @@ -98,8 +98,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 72c66b4fb6..f84a89cdf2 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, @@ -424,37 +423,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. @@ -1219,29 +1187,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. @@ -1922,12 +1867,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..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 (`HostChainSet.network` 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 b91ce6e047..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 (`HostChainSet.network` 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/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..ff410948ba 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,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 `HostChainSet::network` - /// 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 @@ -937,7 +941,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 +994,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 +1005,35 @@ 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 + /// [`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, + asset_hub: Some(self.asset_hub_genesis), + people: self.people_genesis, + bulletin: self.bulletin_genesis, + }; + Stack::new( + self.network.to_string(), + self.network_suffix.to_string(), + chains, + !self.disposable_identities, + ) } /// 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..ccd4de1ba2 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,55 @@ fn test_refresh_expired_error() { let published = expired(11); assert!(refresh(&sign_copy(10, "g1"), Fetched::Document(&published)).is_err()); } + +/// 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 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_config(&dot).unwrap().stack(), + Err(StackError::Reserved { value: "tld dot".to_string() }) + ); +} + +/// 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 8cb73ae0f7..2a15a8b881 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 6e46150919..8053934aa4 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 7066ad1861..78e7285f99 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, @@ -1776,80 +1781,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. @@ -2023,9 +2024,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, @@ -2215,9 +2213,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 463e2d617e..e5c87c6cd5 100644 --- a/rust/crates/truapi/src/runtime.rs +++ b/rust/crates/truapi/src/runtime.rs @@ -772,9 +772,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") @@ -816,9 +814,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/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/pairing_host/tests.rs b/rust/crates/truapi/src/runtime/pairing_host/tests.rs index 85b736e8fc..8ff21bfc21 100644 --- a/rust/crates/truapi/src/runtime/pairing_host/tests.rs +++ b/rust/crates/truapi/src/runtime/pairing_host/tests.rs @@ -46,9 +46,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); @@ -103,9 +101,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); @@ -153,9 +149,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 0bba3eda5b..657235e401 100644 --- a/rust/crates/truapi/src/runtime/signing_host.rs +++ b/rust/crates/truapi/src/runtime/signing_host.rs @@ -8,7 +8,7 @@ mod sso_service; 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, @@ -80,10 +79,6 @@ use zeroize::Zeroizing; 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. @@ -115,15 +110,14 @@ 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()); let session_state = SessionState::new(); Arc::new(Self { services, platform: platform.clone(), - network_suffix, #[cfg(feature = "test-host")] resource_controls: Arc::default(), #[cfg(feature = "test-host")] @@ -197,16 +191,13 @@ 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(), ); let session_state = SessionState::new(); 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")] @@ -229,10 +220,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 @@ -386,7 +377,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 identity_keypair_for( @@ -486,11 +477,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()), }, ]) } @@ -500,16 +491,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/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 d396ba6aee..772ec8f664 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 { @@ -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], + 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) } @@ -2619,7 +2619,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 bdd32c76b8..6375e34036 100644 --- a/rust/crates/truapi/src/runtime/sso_request_service.rs +++ b/rust/crates/truapi/src/runtime/sso_request_service.rs @@ -283,7 +283,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, session, ) .await; @@ -340,7 +340,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, session, ) .await; @@ -568,13 +568,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, 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..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.asset_hub_chain_genesis_hash, + self.host.host_config.stack.chains().asset_hub, 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 89fad92728..0fbdb6ee33 100644 --- a/rust/crates/truapi/src/runtime/sso_responder_service/tests.rs +++ b/rust/crates/truapi/src/runtime/sso_responder_service/tests.rs @@ -71,22 +71,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, ); @@ -880,9 +885,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, ); @@ -1202,9 +1205,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, ); @@ -1503,9 +1504,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()); @@ -1650,9 +1649,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 { @@ -1759,9 +1756,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()); @@ -1841,9 +1836,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()); @@ -1925,9 +1918,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()); @@ -2014,9 +2005,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()); @@ -2116,9 +2105,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); @@ -2460,9 +2447,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); @@ -2730,9 +2715,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())); @@ -3369,9 +3352,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); @@ -3479,9 +3460,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); @@ -7079,9 +7058,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); @@ -7163,9 +7140,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); @@ -7232,9 +7207,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); @@ -7286,7 +7259,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(|| { @@ -7326,7 +7299,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")), @@ -7356,7 +7329,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(|| { @@ -7376,18 +7349,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); @@ -7578,9 +7554,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, ); @@ -7805,9 +7779,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..b8ba7ef073 --- /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, 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}")] + 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..b8a2ef7bb8 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,11 +749,23 @@ 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"))) } +#[cfg(feature = "test-host")] fn get_optional_bytes32_at( value: &JsValue, name: &str, @@ -736,11 +789,13 @@ fn get_optional_bytes32_at( }) } +#[cfg(feature = "test-host")] fn get_required_bytes32_at(value: &JsValue, name: &str, path: &str) -> 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 { 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(),