@parity/product-sdk-host 0.18.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/testing.d.ts CHANGED
@@ -17,8 +17,9 @@ import '@parity/truapi/sandbox';
17
17
  * (and `getChainAPI()` on top of it) resolves in tests; see the `chainInfo`
18
18
  * option. Not modeled: the rest of the PAPI `chain` JSON-RPC surface behind
19
19
  * `getHostProvider()` — there's no chain-read fake, by design; the host owns RPC
20
- * selection — and the `chat` / `coinPayment` / `entropy` / `notifications` /
21
- * `payment` / `permissions` / `resourceAllocation` / `theme` domains. Touching
20
+ * selection — the `system` domain's `info` / `getProductContext`, and the
21
+ * `chat` / `coinPayment` / `entropy` / `locale` / `notifications` / `payment` /
22
+ * `permissions` / `resourceAllocation` / `theme` domains. Touching
22
23
  * an unmodeled domain throws a descriptive error rather than failing with
23
24
  * `undefined is not a function`.
24
25
  *
package/dist/testing.js CHANGED
@@ -111,11 +111,12 @@ function createFakeTruApiClient(options) {
111
111
  createProofAuthorized: () => okAsync({ proof: { tag: "Sr25519", value: { signature, signer: publicKey } } }),
112
112
  submit: () => okAsync(void 0)
113
113
  },
114
- system: {
114
+ // `info` and `getProductContext` are not modeled; they still throw.
115
+ system: notModeled("system", {
115
116
  handshake: () => okAsync(void 0),
116
117
  featureSupported: () => okAsync({ supported: chainSupported }),
117
118
  navigateTo: () => okAsync(void 0)
118
- },
119
+ }),
119
120
  preimage: {
120
121
  lookupSubscribe: ({ request: { key } }) => oneShotObservable({ value: preimages.get(key) }),
121
122
  submit: (value) => {
@@ -141,6 +142,7 @@ function createFakeTruApiClient(options) {
141
142
  chat: notModeled("chat"),
142
143
  coinPayment: notModeled("coinPayment"),
143
144
  entropy: notModeled("entropy"),
145
+ locale: notModeled("locale"),
144
146
  notifications: notModeled("notifications"),
145
147
  payment: notModeled("payment"),
146
148
  permissions: notModeled("permissions"),
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/testing.ts"],"names":[],"mappings":"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","file":"testing.js","sourcesContent":["// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Test fakes for `@parity/product-sdk-host`.\n *\n * The host reaches its container through one cached `TrUApiClient`.\n * `createFakeHost()` injects a fake via `setTruApiClient` so every accessor\n * (`getHostLocalStorage`, `getAccountsProvider`, `getStatementStore`, `getTruApi`,\n * …) resolves against it and `isInsideContainer()` reports `true` — which also\n * makes a default `SignerManager`, `local-storage` auto-detection, and the\n * `statement-store` / `cloud-storage` host paths testable.\n *\n * Of the `chain` domain only `getChainInfo` is modeled, so host chain discovery\n * (and `getChainAPI()` on top of it) resolves in tests; see the `chainInfo`\n * option. Not modeled: the rest of the PAPI `chain` JSON-RPC surface behind\n * `getHostProvider()` — there's no chain-read fake, by design; the host owns RPC\n * selection — and the `chat` / `coinPayment` / `entropy` / `notifications` /\n * `payment` / `permissions` / `resourceAllocation` / `theme` domains. Touching\n * an unmodeled domain throws a descriptive error rather than failing with\n * `undefined is not a function`.\n *\n * @packageDocumentation\n */\nimport type { ObservableLike, Observer, Subscription, TrUApiClient } from \"@parity/truapi\";\nimport { errAsync, okAsync } from \"neverthrow\";\n\nimport type { HostChainIdentifier } from \"./chain-discovery.js\";\n\nimport { type HostConnectionStatus, emitConnectionStatus, setTruApiClient } from \"./transport.js\";\n\nexport { setTruApiClient, emitConnectionStatus };\n\n/**\n * The public surface of a generated truapi domain client. `keyof` skips private\n * members, so this is the shape a plain object can implement — the classes\n * themselves are unimplementable outside truapi because of their\n * `private transport`.\n */\ntype PublicSurface<C> = { [K in keyof C]: C[K] };\n\n/**\n * `TrUApiClient` seen through public surfaces only. The fake is checked against\n * this member-by-member, so drift from the generated client — a renamed method,\n * a changed request or response type — fails compilation here instead of\n * surfacing as a runtime mismatch in consumers' tests.\n */\ntype PublicTruApiClient = { [D in keyof TrUApiClient]: PublicSurface<TrUApiClient[D]> };\n\n/** Inlined to keep the `truapi` facade out of this entry's bundle. */\nfunction toHex(bytes: Uint8Array): `0x${string}` {\n return `0x${Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\nfunction fakeSubscription(): Subscription {\n return { unsubscribe: () => {}, subscriptionId: \"fake-subscription\" };\n}\n\n/**\n * Wrap a `subscribe` implementation into a full `ObservableLike`. The\n * `Symbol.observable` interop member only lands on the object when the symbol\n * is polyfilled (e.g. by rxjs) — same as the generated client — and nothing in\n * the SDK reads it; it exists to satisfy the interface.\n */\nfunction makeObservable<Item, Reason = never>(\n subscribe: (observer?: Partial<Observer<Item, Reason>>) => Subscription,\n): ObservableLike<Item, Reason> {\n const observable: ObservableLike<Item, Reason> = {\n subscribe,\n [Symbol.observable]() {\n return observable;\n },\n };\n return observable;\n}\n\n/** An `ObservableLike` that never emits. Drive statement delivery with `createFakeStatementTransport`. */\nfunction inertObservable<Item, Reason = never>(): ObservableLike<Item, Reason> {\n return makeObservable(() => fakeSubscription());\n}\n\n/**\n * An `ObservableLike` that emits `item` once, asynchronously. The microtask defer\n * matters: the caller assigns its subscription handle from the return value, so\n * the item must arrive after `subscribe()` returns, not during it.\n */\nfunction oneShotObservable<Item>(item: Item): ObservableLike<Item> {\n return makeObservable((observer) => {\n queueMicrotask(() => observer?.next?.(item));\n return fakeSubscription();\n });\n}\n\n/**\n * A domain the fake deliberately doesn't model (see the module header). Any\n * member access throws with a pointer here, instead of the bare TypeError an\n * empty stub would give. The empty-object cast is the one concession a Proxy\n * needs; every modeled domain is checked structurally.\n *\n * `modeled` carries the members that _are_ implemented, for a domain where the\n * fake covers some calls but not the whole surface.\n */\nfunction notModeled<D extends keyof TrUApiClient>(\n domain: D,\n modeled?: Partial<PublicSurface<TrUApiClient[D]>>,\n): PublicSurface<TrUApiClient[D]> {\n return new Proxy((modeled ?? {}) as PublicSurface<TrUApiClient[D]>, {\n get(target, member) {\n if (member in target) return target[member as keyof typeof target];\n // Stay quiet for inspection probes (console.log, await-resolution).\n if (typeof member === \"symbol\" || member === \"then\") return undefined;\n throw new Error(\n `createFakeTruApiClient: \\`${domain}.${member}\\` is not modeled by the fake. See the @parity/product-sdk-host/testing module docs for what is covered.`,\n );\n },\n });\n}\n\n/** Deterministic `0x`-prefixed preimage key derived from a hex value (FNV-1a). */\nfunction preimageKey(hexValue: string): `0x${string}` {\n let h = 0x811c9dc5;\n for (let i = 0; i < hexValue.length; i++) {\n h ^= hexValue.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return `0x${(h >>> 0).toString(16).padStart(8, \"0\")}`;\n}\n\n/**\n * What a fake host reports through `chain.getChainInfo`: the network id it is\n * configured for, and a genesis hash per chain role it serves.\n *\n * Roles left out are answered `NotSupported`, exactly as a host that does not\n * serve them. Use the genesis hashes the descriptors expose (e.g.\n * `paseo_asset_hub.genesis`) if the test drives `getChainAPI()`, since the\n * environment is derived by matching the asset hub genesis against the bundle.\n */\nexport interface FakeChainInfo {\n /** Ecosystem the fake host claims, e.g. `\"paseo\"`. */\n network: string;\n /** Genesis hash per chain role served. */\n chains: Partial<Record<HostChainIdentifier, `0x${string}`>>;\n}\n\n/** Options for {@link createFakeTruApiClient}. */\nexport interface CreateFakeTruApiClientOptions {\n /** `account.getUserId` primary username. Default `\"alice.dot\"`. */\n primaryUsername?: string;\n /** Product-account public key. Default 32 bytes of `0x11`. */\n publicKey?: Uint8Array;\n /** Bytes returned by the signing domain (signatures / proofs / transactions). Default 64 bytes of `0x22`. */\n signature?: Uint8Array;\n /** Whether `system.featureSupported` reports the chain supported. Default `true`. */\n chainSupported?: boolean;\n /** Seed the in-memory host localStorage, keyed by storage key. */\n localStorage?: Record<string, Uint8Array>;\n /** Legacy accounts returned by `account.getLegacyAccounts`. Default none. */\n legacyAccounts?: Array<{ publicKey: Uint8Array; name: string }>;\n /** Seed the in-memory preimage store, keyed by the `0x` preimage key. */\n preimages?: Record<string, Uint8Array>;\n /**\n * What `chain.getChainInfo` reports. Omit to model a host predating chain\n * discovery: the call is refused as `Unsupported`, so `getHostChainInfo`\n * resolves `null` and `getChainAPI` needs an explicit environment.\n */\n chainInfo?: FakeChainInfo;\n}\n\n/**\n * Build a fake `TrUApiClient` covering the domains the host accessors use:\n * `localStorage` (real in-memory KV), `account` / `signing` (canned data),\n * `statementStore`, `preimage`, and `system`. Unmodeled domains (`chain` et al —\n * see the module header) throw on member access.\n */\nexport function createFakeTruApiClient(options?: CreateFakeTruApiClientOptions): TrUApiClient {\n const primaryUsername = options?.primaryUsername ?? \"alice.dot\";\n const publicKey = toHex(options?.publicKey ?? new Uint8Array(32).fill(0x11));\n const signature = toHex(options?.signature ?? new Uint8Array(64).fill(0x22));\n const chainSupported = options?.chainSupported ?? true;\n const chainInfo = options?.chainInfo;\n\n // Real in-memory KV (hex values) so getHostLocalStorage()/createLocalKvStore() round-trip.\n const kv = new Map<string, `0x${string}`>();\n for (const [key, value] of Object.entries(options?.localStorage ?? {})) {\n kv.set(key, toHex(value));\n }\n\n const legacyAccounts = (options?.legacyAccounts ?? []).map((a) => ({\n publicKey: toHex(a.publicKey),\n name: a.name,\n }));\n\n // Preimage store (hex key -> hex value), for getPreimageManager() and submit/lookup round-trips.\n const preimages = new Map<string, `0x${string}`>();\n for (const [key, value] of Object.entries(options?.preimages ?? {})) {\n preimages.set(key, toHex(value));\n }\n\n // Typed against the generated client's public surface: every method below is\n // structurally checked, so a truapi signature change breaks this file's build,\n // not a consumer's test run.\n const client: PublicTruApiClient = {\n localStorage: {\n read: ({ key }) => okAsync({ value: kv.get(key) }),\n write: ({ key, value }) => {\n kv.set(key, value);\n return okAsync(undefined);\n },\n clear: ({ key }) => {\n kv.delete(key);\n return okAsync(undefined);\n },\n },\n account: {\n getUserId: () => okAsync({ primaryUsername }),\n requestLogin: () => okAsync(\"Success\"),\n getAccount: () => okAsync({ account: { publicKey } }),\n getAccountAlias: () =>\n okAsync({ context: toHex(new Uint8Array([1])), alias: toHex(new Uint8Array([2])) }),\n getLegacyAccounts: () => okAsync({ accounts: legacyAccounts }),\n createAccountProof: () =>\n okAsync({\n proof: signature,\n contextualAlias: {\n context: toHex(new Uint8Array([1])),\n alias: toHex(new Uint8Array([2])),\n },\n ringIndex: 0,\n ringRevision: 0,\n }),\n registerRingVrfKey: () => okAsync(publicKey),\n listRingVrfKeys: () => okAsync([]),\n ringVrfSign: () => okAsync(signature),\n signVrf: () => okAsync({ preOutput: publicKey, proof: signature }),\n connectionStatusSubscribe: () => inertObservable(),\n },\n signing: {\n createTransaction: () => okAsync({ transaction: signature }),\n createTransactionWithLegacyAccount: () => okAsync({ transaction: signature }),\n signRaw: () => okAsync({ signature }),\n signRawWithLegacyAccount: () => okAsync({ signature }),\n signPayload: () => okAsync({ signature }),\n signPayloadWithLegacyAccount: () => okAsync({ signature }),\n },\n statementStore: {\n subscribe: () => inertObservable(),\n createProof: () =>\n okAsync({ proof: { tag: \"Sr25519\", value: { signature, signer: publicKey } } }),\n createProofAuthorized: () =>\n okAsync({ proof: { tag: \"Sr25519\", value: { signature, signer: publicKey } } }),\n submit: () => okAsync(undefined),\n },\n system: {\n handshake: () => okAsync(undefined),\n featureSupported: () => okAsync({ supported: chainSupported }),\n navigateTo: () => okAsync(undefined),\n },\n preimage: {\n lookupSubscribe: ({ request: { key } }) =>\n oneShotObservable({ value: preimages.get(key) }),\n submit: (value) => {\n const key = preimageKey(value);\n preimages.set(key, value);\n return okAsync(key);\n },\n },\n // Only `getChainInfo` is modeled; the rest of the domain still throws.\n chain: notModeled(\"chain\", {\n getChainInfo: ({ chain }) => {\n if (!chainInfo) return errAsync({ tag: \"Unsupported\" } as const);\n const genesisHash = chainInfo.chains[chain];\n if (!genesisHash) {\n return errAsync({\n tag: \"Domain\",\n value: { tag: \"V1\", value: { tag: \"NotSupported\" } },\n } as const);\n }\n return okAsync({ network: chainInfo.network, chain, genesisHash });\n },\n }),\n chat: notModeled(\"chat\"),\n coinPayment: notModeled(\"coinPayment\"),\n entropy: notModeled(\"entropy\"),\n notifications: notModeled(\"notifications\"),\n payment: notModeled(\"payment\"),\n permissions: notModeled(\"permissions\"),\n resourceAllocation: notModeled(\"resourceAllocation\"),\n theme: notModeled(\"theme\"),\n };\n\n // A plain downcast, not an `unknown` bridge: each generated class is\n // assignable to its `PublicSurface`, and TS has already verified every fake\n // member against the generated signatures in the annotation above.\n return client as TrUApiClient;\n}\n\n/**\n * A `beforeEach`-registered test hook, e.g. Vitest's `onTestFinished`. Read off\n * `globalThis` so we couple to no test framework: present under Vitest's globals\n * mode, absent everywhere else (Jest, `node:test`, plain scripts).\n */\ntype TestFinishedHook = (fn: () => void) => void;\n\n/** Handle returned by {@link createFakeHost}. */\nexport interface FakeHost extends Disposable {\n /** The injected fake client (also what `getTruApi()` returns). */\n client: TrUApiClient;\n /**\n * Push a status to `subscribeConnectionStatus` subscribers, so a product's\n * reconnecting / offline UI can be exercised. `dispose()` already reports\n * `\"disconnected\"`; use this to drive the states in between.\n */\n emitConnectionStatus(status: HostConnectionStatus): void;\n /** Clear the override. Idempotent; safe to call more than once. */\n dispose(): void;\n /** `using host = createFakeHost()` restores the real client at scope end. */\n [Symbol.dispose](): void;\n}\n\n/**\n * Inject a fake client via {@link setTruApiClient} and return a disposer.\n *\n * Cleanup is guaranteed three ways, so a forgotten reset can't leak the override\n * into the next test or file: prefer `using` (scope-end disposal), otherwise the\n * handle self-registers `onTestFinished` when created inside a Vitest run, and\n * `dispose()` is always there to call by hand.\n *\n * @example\n * ```ts\n * import { createFakeHost } from \"@parity/product-sdk-host/testing\";\n *\n * test(\"host storage is reachable\", async () => {\n * using host = createFakeHost({ primaryUsername: \"carol.dot\" });\n * // getHostLocalStorage(), getAccountsProvider(), a default SignerManager, etc.\n * // resolve the fake; the real client is restored when the test scope ends.\n * });\n * ```\n */\nexport function createFakeHost(options?: CreateFakeTruApiClientOptions): FakeHost {\n const client = createFakeTruApiClient(options);\n setTruApiClient(client);\n\n let disposed = false;\n const dispose = () => {\n if (disposed) return;\n disposed = true;\n setTruApiClient(null);\n };\n\n // Best-effort auto-cleanup for the common case (a bare `const host =\n // createFakeHost()` with no `using` and no manual reset). Under Vitest's\n // globals mode `onTestFinished` is on `globalThis`; elsewhere it's absent and\n // we quietly rely on `using` / manual `dispose()`.\n const onTestFinished = (globalThis as { onTestFinished?: TestFinishedHook }).onTestFinished;\n if (typeof onTestFinished === \"function\") {\n try {\n onTestFinished(dispose);\n } catch {\n // Not inside a running test (e.g. called at module scope) — ignore.\n }\n }\n\n return {\n client,\n emitConnectionStatus,\n dispose,\n [Symbol.dispose]: dispose,\n };\n}\n\nif (import.meta.vitest) {\n // Round-trip guard: drive the fake through the *real* host accessors.\n const { describe, test, expect, afterEach } = import.meta.vitest;\n const { getHostLocalStorage, getStatementStore, isInsideContainer } = await import(\n \"./container.js\"\n );\n const { getAccountsProvider } = await import(\"./accounts.js\");\n const { getHostChainInfo } = await import(\"./chain-discovery.js\");\n const { getPreimageManager } = await import(\"./truapi.js\");\n\n const lookupOnce = (\n manager: NonNullable<Awaited<ReturnType<typeof getPreimageManager>>>,\n key: `0x${string}`,\n ) =>\n new Promise<Uint8Array | null>((resolve) => {\n manager.lookup(key, (preimage) => {\n if (preimage !== null) resolve(preimage);\n });\n });\n\n afterEach(() => setTruApiClient(null));\n\n describe(\"createFakeHost / createFakeTruApiClient\", () => {\n test(\"host localStorage round-trips through the real adapter\", async () => {\n createFakeHost();\n const ls = await getHostLocalStorage();\n expect(ls).not.toBeNull();\n await ls?.writeString(\"k\", \"v\");\n expect(await ls?.readString(\"k\")).toBe(\"v\");\n await ls?.writeJSON(\"j\", { a: 1 });\n expect(await ls?.readJSON(\"j\")).toEqual({ a: 1 });\n expect(await ls?.readString(\"missing\")).toBe(\"\");\n });\n\n test(\"seeded localStorage is readable\", async () => {\n createFakeHost({ localStorage: { greeting: new TextEncoder().encode(\"hi\") } });\n const ls = await getHostLocalStorage();\n expect(await ls?.readString(\"greeting\")).toBe(\"hi\");\n });\n\n test(\"accounts provider resolves the configured user\", async () => {\n createFakeHost({ primaryUsername: \"carol.dot\" });\n const accounts = await getAccountsProvider();\n expect(accounts).not.toBeNull();\n const userId = await accounts?.getUserId().match(\n (v) => v,\n () => null,\n );\n expect(userId?.primaryUsername).toBe(\"carol.dot\");\n });\n\n test(\"signVrf round-trips through the real adapter\", async () => {\n // The fake is the only way a product can test a VRF flow: there is no\n // dev-provider implementation and the e2e test host does not expose the\n // call. So the fake's wire shape has to stay decodable by the adapter.\n createFakeHost();\n const accounts = await getAccountsProvider();\n const signature = await accounts\n ?.signVrf({ dotNsIdentifier: \"app.dot\" }, new Uint8Array([1]), [\n { label: new Uint8Array([2]), value: new Uint8Array([3]) },\n ])\n .match(\n (s) => s,\n () => null,\n );\n expect(signature?.preOutput).toBeInstanceOf(Uint8Array);\n expect(signature?.proof).toBeInstanceOf(Uint8Array);\n expect(signature?.preOutput).toHaveLength(32);\n expect(signature?.proof).toHaveLength(64);\n });\n\n test(\"chain discovery is refused by default, so a legacy host is modeled\", async () => {\n createFakeHost();\n // Refused, not unmodeled: a bare `getChainAPI(\"paseo\")` in a consumer\n // test must not warn about the fake on every call.\n expect(await getHostChainInfo([\"AssetHub\"])).toBeNull();\n });\n\n test(\"chainInfo drives discovery, with unserved roles left out\", async () => {\n createFakeHost({\n chainInfo: { network: \"paseo\", chains: { AssetHub: \"0xaa\", Bulletin: \"0xbb\" } },\n });\n expect(await getHostChainInfo([\"AssetHub\", \"Bulletin\", \"People\"])).toEqual({\n network: \"paseo\",\n chains: { AssetHub: \"0xaa\", Bulletin: \"0xbb\" },\n });\n });\n\n test(\"the rest of the chain domain still reports itself unmodeled\", async () => {\n const host = createFakeHost();\n expect(() => (host.client.chain as { chainName?: unknown }).chainName).toThrow(\n /not modeled by the fake/,\n );\n });\n\n test(\"statement store resolves\", async () => {\n createFakeHost();\n expect(await getStatementStore()).not.toBeNull();\n });\n\n test(\"preimage manager reads seeded bytes and round-trips submit -> lookup\", async () => {\n createFakeHost({ preimages: { \"0xabc\": new Uint8Array([1, 2, 3]) } });\n const pm = await getPreimageManager();\n expect(pm).not.toBeNull();\n\n const seeded = await lookupOnce(pm!, \"0xabc\");\n expect(Array.from(seeded ?? [])).toEqual([1, 2, 3]);\n\n const key = await pm!.submit(new Uint8Array([9, 9]));\n const back = await lookupOnce(pm!, key);\n expect(Array.from(back ?? [])).toEqual([9, 9]);\n });\n\n test(\"isInsideContainer is true while set, false after dispose\", async () => {\n const host = createFakeHost();\n expect(await isInsideContainer()).toBe(true);\n host.dispose();\n expect(await isInsideContainer()).toBe(false);\n expect(await getHostLocalStorage()).toBeNull();\n });\n\n test(\"Symbol.dispose clears the override and dispose is idempotent\", async () => {\n {\n using host = createFakeHost();\n expect(await isInsideContainer()).toBe(true);\n host.dispose(); // explicit + scope-end Symbol.dispose must not double-clear badly\n }\n expect(await isInsideContainer()).toBe(false);\n });\n });\n}\n"]}
1
+ {"version":3,"sources":["../src/testing.ts"],"names":[],"mappings":"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","file":"testing.js","sourcesContent":["// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Test fakes for `@parity/product-sdk-host`.\n *\n * The host reaches its container through one cached `TrUApiClient`.\n * `createFakeHost()` injects a fake via `setTruApiClient` so every accessor\n * (`getHostLocalStorage`, `getAccountsProvider`, `getStatementStore`, `getTruApi`,\n * …) resolves against it and `isInsideContainer()` reports `true` — which also\n * makes a default `SignerManager`, `local-storage` auto-detection, and the\n * `statement-store` / `cloud-storage` host paths testable.\n *\n * Of the `chain` domain only `getChainInfo` is modeled, so host chain discovery\n * (and `getChainAPI()` on top of it) resolves in tests; see the `chainInfo`\n * option. Not modeled: the rest of the PAPI `chain` JSON-RPC surface behind\n * `getHostProvider()` — there's no chain-read fake, by design; the host owns RPC\n * selection — the `system` domain's `info` / `getProductContext`, and the\n * `chat` / `coinPayment` / `entropy` / `locale` / `notifications` / `payment` /\n * `permissions` / `resourceAllocation` / `theme` domains. Touching\n * an unmodeled domain throws a descriptive error rather than failing with\n * `undefined is not a function`.\n *\n * @packageDocumentation\n */\nimport type { ObservableLike, Observer, Subscription, TrUApiClient } from \"@parity/truapi\";\nimport { errAsync, okAsync } from \"neverthrow\";\n\nimport type { HostChainIdentifier } from \"./chain-discovery.js\";\n\nimport { type HostConnectionStatus, emitConnectionStatus, setTruApiClient } from \"./transport.js\";\n\nexport { setTruApiClient, emitConnectionStatus };\n\n/**\n * The public surface of a generated truapi domain client. `keyof` skips private\n * members, so this is the shape a plain object can implement — the classes\n * themselves are unimplementable outside truapi because of their\n * `private transport`.\n */\ntype PublicSurface<C> = { [K in keyof C]: C[K] };\n\n/**\n * `TrUApiClient` seen through public surfaces only. The fake is checked against\n * this member-by-member, so drift from the generated client — a renamed method,\n * a changed request or response type — fails compilation here instead of\n * surfacing as a runtime mismatch in consumers' tests.\n */\ntype PublicTruApiClient = { [D in keyof TrUApiClient]: PublicSurface<TrUApiClient[D]> };\n\n/** Inlined to keep the `truapi` facade out of this entry's bundle. */\nfunction toHex(bytes: Uint8Array): `0x${string}` {\n return `0x${Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\nfunction fakeSubscription(): Subscription {\n return { unsubscribe: () => {}, subscriptionId: \"fake-subscription\" };\n}\n\n/**\n * Wrap a `subscribe` implementation into a full `ObservableLike`. The\n * `Symbol.observable` interop member only lands on the object when the symbol\n * is polyfilled (e.g. by rxjs) — same as the generated client — and nothing in\n * the SDK reads it; it exists to satisfy the interface.\n */\nfunction makeObservable<Item, Reason = never>(\n subscribe: (observer?: Partial<Observer<Item, Reason>>) => Subscription,\n): ObservableLike<Item, Reason> {\n const observable: ObservableLike<Item, Reason> = {\n subscribe,\n [Symbol.observable]() {\n return observable;\n },\n };\n return observable;\n}\n\n/** An `ObservableLike` that never emits. Drive statement delivery with `createFakeStatementTransport`. */\nfunction inertObservable<Item, Reason = never>(): ObservableLike<Item, Reason> {\n return makeObservable(() => fakeSubscription());\n}\n\n/**\n * An `ObservableLike` that emits `item` once, asynchronously. The microtask defer\n * matters: the caller assigns its subscription handle from the return value, so\n * the item must arrive after `subscribe()` returns, not during it.\n */\nfunction oneShotObservable<Item>(item: Item): ObservableLike<Item> {\n return makeObservable((observer) => {\n queueMicrotask(() => observer?.next?.(item));\n return fakeSubscription();\n });\n}\n\n/**\n * A domain the fake deliberately doesn't model (see the module header). Any\n * member access throws with a pointer here, instead of the bare TypeError an\n * empty stub would give. The empty-object cast is the one concession a Proxy\n * needs; every modeled domain is checked structurally.\n *\n * `modeled` carries the members that _are_ implemented, for a domain where the\n * fake covers some calls but not the whole surface.\n */\nfunction notModeled<D extends keyof TrUApiClient>(\n domain: D,\n modeled?: Partial<PublicSurface<TrUApiClient[D]>>,\n): PublicSurface<TrUApiClient[D]> {\n return new Proxy((modeled ?? {}) as PublicSurface<TrUApiClient[D]>, {\n get(target, member) {\n if (member in target) return target[member as keyof typeof target];\n // Stay quiet for inspection probes (console.log, await-resolution).\n if (typeof member === \"symbol\" || member === \"then\") return undefined;\n throw new Error(\n `createFakeTruApiClient: \\`${domain}.${member}\\` is not modeled by the fake. See the @parity/product-sdk-host/testing module docs for what is covered.`,\n );\n },\n });\n}\n\n/** Deterministic `0x`-prefixed preimage key derived from a hex value (FNV-1a). */\nfunction preimageKey(hexValue: string): `0x${string}` {\n let h = 0x811c9dc5;\n for (let i = 0; i < hexValue.length; i++) {\n h ^= hexValue.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return `0x${(h >>> 0).toString(16).padStart(8, \"0\")}`;\n}\n\n/**\n * What a fake host reports through `chain.getChainInfo`: the network id it is\n * configured for, and a genesis hash per chain role it serves.\n *\n * Roles left out are answered `NotSupported`, exactly as a host that does not\n * serve them. Use the genesis hashes the descriptors expose (e.g.\n * `paseo_asset_hub.genesis`) if the test drives `getChainAPI()`, since the\n * environment is derived by matching the asset hub genesis against the bundle.\n */\nexport interface FakeChainInfo {\n /** Ecosystem the fake host claims, e.g. `\"paseo\"`. */\n network: string;\n /** Genesis hash per chain role served. */\n chains: Partial<Record<HostChainIdentifier, `0x${string}`>>;\n}\n\n/** Options for {@link createFakeTruApiClient}. */\nexport interface CreateFakeTruApiClientOptions {\n /** `account.getUserId` primary username. Default `\"alice.dot\"`. */\n primaryUsername?: string;\n /** Product-account public key. Default 32 bytes of `0x11`. */\n publicKey?: Uint8Array;\n /** Bytes returned by the signing domain (signatures / proofs / transactions). Default 64 bytes of `0x22`. */\n signature?: Uint8Array;\n /** Whether `system.featureSupported` reports the chain supported. Default `true`. */\n chainSupported?: boolean;\n /** Seed the in-memory host localStorage, keyed by storage key. */\n localStorage?: Record<string, Uint8Array>;\n /** Legacy accounts returned by `account.getLegacyAccounts`. Default none. */\n legacyAccounts?: Array<{ publicKey: Uint8Array; name: string }>;\n /** Seed the in-memory preimage store, keyed by the `0x` preimage key. */\n preimages?: Record<string, Uint8Array>;\n /**\n * What `chain.getChainInfo` reports. Omit to model a host predating chain\n * discovery: the call is refused as `Unsupported`, so `getHostChainInfo`\n * resolves `null` and `getChainAPI` needs an explicit environment.\n */\n chainInfo?: FakeChainInfo;\n}\n\n/**\n * Build a fake `TrUApiClient` covering the domains the host accessors use:\n * `localStorage` (real in-memory KV), `account` / `signing` (canned data),\n * `statementStore`, `preimage`, and `system`. Unmodeled domains (`chain` et al —\n * see the module header) throw on member access.\n */\nexport function createFakeTruApiClient(options?: CreateFakeTruApiClientOptions): TrUApiClient {\n const primaryUsername = options?.primaryUsername ?? \"alice.dot\";\n const publicKey = toHex(options?.publicKey ?? new Uint8Array(32).fill(0x11));\n const signature = toHex(options?.signature ?? new Uint8Array(64).fill(0x22));\n const chainSupported = options?.chainSupported ?? true;\n const chainInfo = options?.chainInfo;\n\n // Real in-memory KV (hex values) so getHostLocalStorage()/createLocalKvStore() round-trip.\n const kv = new Map<string, `0x${string}`>();\n for (const [key, value] of Object.entries(options?.localStorage ?? {})) {\n kv.set(key, toHex(value));\n }\n\n const legacyAccounts = (options?.legacyAccounts ?? []).map((a) => ({\n publicKey: toHex(a.publicKey),\n name: a.name,\n }));\n\n // Preimage store (hex key -> hex value), for getPreimageManager() and submit/lookup round-trips.\n const preimages = new Map<string, `0x${string}`>();\n for (const [key, value] of Object.entries(options?.preimages ?? {})) {\n preimages.set(key, toHex(value));\n }\n\n // Typed against the generated client's public surface: every method below is\n // structurally checked, so a truapi signature change breaks this file's build,\n // not a consumer's test run.\n const client: PublicTruApiClient = {\n localStorage: {\n read: ({ key }) => okAsync({ value: kv.get(key) }),\n write: ({ key, value }) => {\n kv.set(key, value);\n return okAsync(undefined);\n },\n clear: ({ key }) => {\n kv.delete(key);\n return okAsync(undefined);\n },\n },\n account: {\n getUserId: () => okAsync({ primaryUsername }),\n requestLogin: () => okAsync(\"Success\"),\n getAccount: () => okAsync({ account: { publicKey } }),\n getAccountAlias: () =>\n okAsync({ context: toHex(new Uint8Array([1])), alias: toHex(new Uint8Array([2])) }),\n getLegacyAccounts: () => okAsync({ accounts: legacyAccounts }),\n createAccountProof: () =>\n okAsync({\n proof: signature,\n contextualAlias: {\n context: toHex(new Uint8Array([1])),\n alias: toHex(new Uint8Array([2])),\n },\n ringIndex: 0,\n ringRevision: 0,\n }),\n registerRingVrfKey: () => okAsync(publicKey),\n listRingVrfKeys: () => okAsync([]),\n ringVrfSign: () => okAsync(signature),\n signVrf: () => okAsync({ preOutput: publicKey, proof: signature }),\n connectionStatusSubscribe: () => inertObservable(),\n },\n signing: {\n createTransaction: () => okAsync({ transaction: signature }),\n createTransactionWithLegacyAccount: () => okAsync({ transaction: signature }),\n signRaw: () => okAsync({ signature }),\n signRawWithLegacyAccount: () => okAsync({ signature }),\n signPayload: () => okAsync({ signature }),\n signPayloadWithLegacyAccount: () => okAsync({ signature }),\n },\n statementStore: {\n subscribe: () => inertObservable(),\n createProof: () =>\n okAsync({ proof: { tag: \"Sr25519\", value: { signature, signer: publicKey } } }),\n createProofAuthorized: () =>\n okAsync({ proof: { tag: \"Sr25519\", value: { signature, signer: publicKey } } }),\n submit: () => okAsync(undefined),\n },\n // `info` and `getProductContext` are not modeled; they still throw.\n system: notModeled(\"system\", {\n handshake: () => okAsync(undefined),\n featureSupported: () => okAsync({ supported: chainSupported }),\n navigateTo: () => okAsync(undefined),\n }),\n preimage: {\n lookupSubscribe: ({ request: { key } }) =>\n oneShotObservable({ value: preimages.get(key) }),\n submit: (value) => {\n const key = preimageKey(value);\n preimages.set(key, value);\n return okAsync(key);\n },\n },\n // Only `getChainInfo` is modeled; the rest of the domain still throws.\n chain: notModeled(\"chain\", {\n getChainInfo: ({ chain }) => {\n if (!chainInfo) return errAsync({ tag: \"Unsupported\" } as const);\n const genesisHash = chainInfo.chains[chain];\n if (!genesisHash) {\n return errAsync({\n tag: \"Domain\",\n value: { tag: \"V1\", value: { tag: \"NotSupported\" } },\n } as const);\n }\n return okAsync({ network: chainInfo.network, chain, genesisHash });\n },\n }),\n chat: notModeled(\"chat\"),\n coinPayment: notModeled(\"coinPayment\"),\n entropy: notModeled(\"entropy\"),\n locale: notModeled(\"locale\"),\n notifications: notModeled(\"notifications\"),\n payment: notModeled(\"payment\"),\n permissions: notModeled(\"permissions\"),\n resourceAllocation: notModeled(\"resourceAllocation\"),\n theme: notModeled(\"theme\"),\n };\n\n // A plain downcast, not an `unknown` bridge: each generated class is\n // assignable to its `PublicSurface`, and TS has already verified every fake\n // member against the generated signatures in the annotation above.\n return client as TrUApiClient;\n}\n\n/**\n * A `beforeEach`-registered test hook, e.g. Vitest's `onTestFinished`. Read off\n * `globalThis` so we couple to no test framework: present under Vitest's globals\n * mode, absent everywhere else (Jest, `node:test`, plain scripts).\n */\ntype TestFinishedHook = (fn: () => void) => void;\n\n/** Handle returned by {@link createFakeHost}. */\nexport interface FakeHost extends Disposable {\n /** The injected fake client (also what `getTruApi()` returns). */\n client: TrUApiClient;\n /**\n * Push a status to `subscribeConnectionStatus` subscribers, so a product's\n * reconnecting / offline UI can be exercised. `dispose()` already reports\n * `\"disconnected\"`; use this to drive the states in between.\n */\n emitConnectionStatus(status: HostConnectionStatus): void;\n /** Clear the override. Idempotent; safe to call more than once. */\n dispose(): void;\n /** `using host = createFakeHost()` restores the real client at scope end. */\n [Symbol.dispose](): void;\n}\n\n/**\n * Inject a fake client via {@link setTruApiClient} and return a disposer.\n *\n * Cleanup is guaranteed three ways, so a forgotten reset can't leak the override\n * into the next test or file: prefer `using` (scope-end disposal), otherwise the\n * handle self-registers `onTestFinished` when created inside a Vitest run, and\n * `dispose()` is always there to call by hand.\n *\n * @example\n * ```ts\n * import { createFakeHost } from \"@parity/product-sdk-host/testing\";\n *\n * test(\"host storage is reachable\", async () => {\n * using host = createFakeHost({ primaryUsername: \"carol.dot\" });\n * // getHostLocalStorage(), getAccountsProvider(), a default SignerManager, etc.\n * // resolve the fake; the real client is restored when the test scope ends.\n * });\n * ```\n */\nexport function createFakeHost(options?: CreateFakeTruApiClientOptions): FakeHost {\n const client = createFakeTruApiClient(options);\n setTruApiClient(client);\n\n let disposed = false;\n const dispose = () => {\n if (disposed) return;\n disposed = true;\n setTruApiClient(null);\n };\n\n // Best-effort auto-cleanup for the common case (a bare `const host =\n // createFakeHost()` with no `using` and no manual reset). Under Vitest's\n // globals mode `onTestFinished` is on `globalThis`; elsewhere it's absent and\n // we quietly rely on `using` / manual `dispose()`.\n const onTestFinished = (globalThis as { onTestFinished?: TestFinishedHook }).onTestFinished;\n if (typeof onTestFinished === \"function\") {\n try {\n onTestFinished(dispose);\n } catch {\n // Not inside a running test (e.g. called at module scope) — ignore.\n }\n }\n\n return {\n client,\n emitConnectionStatus,\n dispose,\n [Symbol.dispose]: dispose,\n };\n}\n\nif (import.meta.vitest) {\n // Round-trip guard: drive the fake through the *real* host accessors.\n const { describe, test, expect, afterEach } = import.meta.vitest;\n const { getHostLocalStorage, getStatementStore, isInsideContainer } = await import(\n \"./container.js\"\n );\n const { getAccountsProvider } = await import(\"./accounts.js\");\n const { getHostChainInfo } = await import(\"./chain-discovery.js\");\n const { getPreimageManager } = await import(\"./truapi.js\");\n\n const lookupOnce = (\n manager: NonNullable<Awaited<ReturnType<typeof getPreimageManager>>>,\n key: `0x${string}`,\n ) =>\n new Promise<Uint8Array | null>((resolve) => {\n manager.lookup(key, (preimage) => {\n if (preimage !== null) resolve(preimage);\n });\n });\n\n afterEach(() => setTruApiClient(null));\n\n describe(\"createFakeHost / createFakeTruApiClient\", () => {\n test(\"host localStorage round-trips through the real adapter\", async () => {\n createFakeHost();\n const ls = await getHostLocalStorage();\n expect(ls).not.toBeNull();\n await ls?.writeString(\"k\", \"v\");\n expect(await ls?.readString(\"k\")).toBe(\"v\");\n await ls?.writeJSON(\"j\", { a: 1 });\n expect(await ls?.readJSON(\"j\")).toEqual({ a: 1 });\n expect(await ls?.readString(\"missing\")).toBe(\"\");\n });\n\n test(\"seeded localStorage is readable\", async () => {\n createFakeHost({ localStorage: { greeting: new TextEncoder().encode(\"hi\") } });\n const ls = await getHostLocalStorage();\n expect(await ls?.readString(\"greeting\")).toBe(\"hi\");\n });\n\n test(\"accounts provider resolves the configured user\", async () => {\n createFakeHost({ primaryUsername: \"carol.dot\" });\n const accounts = await getAccountsProvider();\n expect(accounts).not.toBeNull();\n const userId = await accounts?.getUserId().match(\n (v) => v,\n () => null,\n );\n expect(userId?.primaryUsername).toBe(\"carol.dot\");\n });\n\n test(\"signVrf round-trips through the real adapter\", async () => {\n // The fake is the only way a product can test a VRF flow: there is no\n // dev-provider implementation and the e2e test host does not expose the\n // call. So the fake's wire shape has to stay decodable by the adapter.\n createFakeHost();\n const accounts = await getAccountsProvider();\n const signature = await accounts\n ?.signVrf({ dotNsIdentifier: \"app.dot\" }, new Uint8Array([1]), [\n { label: new Uint8Array([2]), value: new Uint8Array([3]) },\n ])\n .match(\n (s) => s,\n () => null,\n );\n expect(signature?.preOutput).toBeInstanceOf(Uint8Array);\n expect(signature?.proof).toBeInstanceOf(Uint8Array);\n expect(signature?.preOutput).toHaveLength(32);\n expect(signature?.proof).toHaveLength(64);\n });\n\n test(\"chain discovery is refused by default, so a legacy host is modeled\", async () => {\n createFakeHost();\n // Refused, not unmodeled: a bare `getChainAPI(\"paseo\")` in a consumer\n // test must not warn about the fake on every call.\n expect(await getHostChainInfo([\"AssetHub\"])).toBeNull();\n });\n\n test(\"chainInfo drives discovery, with unserved roles left out\", async () => {\n createFakeHost({\n chainInfo: { network: \"paseo\", chains: { AssetHub: \"0xaa\", Bulletin: \"0xbb\" } },\n });\n expect(await getHostChainInfo([\"AssetHub\", \"Bulletin\", \"People\"])).toEqual({\n network: \"paseo\",\n chains: { AssetHub: \"0xaa\", Bulletin: \"0xbb\" },\n });\n });\n\n test(\"the rest of the chain domain still reports itself unmodeled\", async () => {\n const host = createFakeHost();\n expect(() => (host.client.chain as { chainName?: unknown }).chainName).toThrow(\n /not modeled by the fake/,\n );\n });\n\n test(\"statement store resolves\", async () => {\n createFakeHost();\n expect(await getStatementStore()).not.toBeNull();\n });\n\n test(\"preimage manager reads seeded bytes and round-trips submit -> lookup\", async () => {\n createFakeHost({ preimages: { \"0xabc\": new Uint8Array([1, 2, 3]) } });\n const pm = await getPreimageManager();\n expect(pm).not.toBeNull();\n\n const seeded = await lookupOnce(pm!, \"0xabc\");\n expect(Array.from(seeded ?? [])).toEqual([1, 2, 3]);\n\n const key = await pm!.submit(new Uint8Array([9, 9]));\n const back = await lookupOnce(pm!, key);\n expect(Array.from(back ?? [])).toEqual([9, 9]);\n });\n\n test(\"isInsideContainer is true while set, false after dispose\", async () => {\n const host = createFakeHost();\n expect(await isInsideContainer()).toBe(true);\n host.dispose();\n expect(await isInsideContainer()).toBe(false);\n expect(await getHostLocalStorage()).toBeNull();\n });\n\n test(\"Symbol.dispose clears the override and dispose is idempotent\", async () => {\n {\n using host = createFakeHost();\n expect(await isInsideContainer()).toBe(true);\n host.dispose(); // explicit + scope-end Symbol.dispose must not double-clear badly\n }\n expect(await isInsideContainer()).toBe(false);\n });\n });\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parity/product-sdk-host",
3
- "version": "0.18.0",
3
+ "version": "0.19.1",
4
4
  "description": "Host container detection and storage access for Polkadot Desktop and Mobile environments",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -22,14 +22,14 @@
22
22
  "src"
23
23
  ],
24
24
  "dependencies": {
25
- "@parity/truapi": "^0.10.0",
25
+ "@parity/truapi": "^0.13.1",
26
26
  "@polkadot-api/json-rpc-provider": "^0.2.0",
27
27
  "@polkadot-api/substrate-bindings": "^0.20.3",
28
28
  "neverthrow": "^8.2.0",
29
29
  "polkadot-api": "^2.1.6",
30
- "@parity/product-sdk-errors": "0.2.0",
30
+ "@parity/product-sdk-logger": "0.1.1",
31
31
  "@parity/result": "0.2.0",
32
- "@parity/product-sdk-logger": "0.1.1"
32
+ "@parity/product-sdk-errors": "0.2.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "tsup": "^8.5.1",
package/src/accounts.ts CHANGED
@@ -371,57 +371,32 @@ export interface AccountsProvider {
371
371
  ): HostSubscription;
372
372
  }
373
373
 
374
- /** The transaction-extension version used by V5 general transactions. */
375
- const GENERAL_TX_EXT_VERSION = 5;
374
+ const V5_FORMAT_SELECTOR = 5;
376
375
 
377
376
  /**
378
- * Choose the wire `txExtVersion` the **transaction-extension** version the
379
- * host must assemble under, which is distinct from the extrinsic *format*
380
- * version (4 / 5).
381
- *
382
- * - `formatVersions` `metadata.extrinsic.version`, the extrinsic *formats* the
383
- * runtime accepts (e.g. `[4]`, `[5]`, `[4, 5]`).
384
- * - `txExtVersions` — the keys of `metadata.extrinsic.signedExtensions`, the
385
- * transaction-extension versions the runtime supports (its v16
386
- * `transactionExtensionsByVersion`; always includes `0`).
387
- *
388
- * A V4 signed extrinsic always uses transaction-extension version `0` (a fixed
389
- * sentinel, independent of what the extension map contains). Prefer V4 while it
390
- * is offered — it carries the account signature in its envelope, and the host
391
- * can build it. Only when V4 is absent do we fall to a V5 general transaction,
392
- * whose transaction-extension version is `5` (the key the runtime and host
393
- * agree on for the general format); require it to actually be present in the
394
- * map rather than assuming it.
395
- *
396
- * Passing the extrinsic *format* number here was the bug (host-rust-core#528):
397
- * `5` is both a format version and the general extension version, so it looked
398
- * right, but the two are unrelated in general and the host decodes extension
399
- * values by this number.
377
+ * Pick the `txExtVersion` for the host's `create_transaction` from the extrinsic formats
378
+ * the runtime offers. The host treats the field as a format switch: `0` builds V4, `5`
379
+ * builds a V5 general transaction, anything else is `NotSupported`. Prefer V4 while
380
+ * offered, since it carries the account signature in its envelope. The host derives the
381
+ * transaction-extension version from the metadata itself.
400
382
  */
401
- function selectHostTxExtVersion(
402
- formatVersions: readonly number[],
403
- txExtVersions: readonly number[],
404
- ): number {
383
+ function selectHostTxExtVersion(formatVersions: readonly number[]): number {
405
384
  if (formatVersions.length === 0) {
406
385
  throw new Error("No extrinsic version found in metadata");
407
386
  }
408
387
  if (formatVersions.includes(4)) {
409
388
  return 0;
410
389
  }
411
- if (txExtVersions.includes(GENERAL_TX_EXT_VERSION)) {
412
- return GENERAL_TX_EXT_VERSION;
390
+ if (formatVersions.includes(5)) {
391
+ return V5_FORMAT_SELECTOR;
413
392
  }
414
393
  throw new Error(
415
- `Runtime offers no V4 extrinsic and no transaction-extension version ${GENERAL_TX_EXT_VERSION} ` +
416
- `(supported: ${txExtVersions.join(", ") || "none"}); cannot select a txExtVersion the host can assemble.`,
394
+ `Runtime offers no extrinsic format 4 or 5 (offers: ${formatVersions.join(", ")}); the host protocol has no txExtVersion for it.`,
417
395
  );
418
396
  }
419
397
 
420
- /** Derive the host's transaction-extension version from SCALE metadata. */
421
398
  function deriveTxExtVersion(metadata: Uint8Array): number {
422
- const extrinsic = unifyMetadata(decAnyMetadata(metadata)).extrinsic;
423
- const txExtVersions = Object.keys(extrinsic.signedExtensions).map(Number);
424
- return selectHostTxExtVersion(extrinsic.version, txExtVersions);
399
+ return selectHostTxExtVersion(unifyMetadata(decAnyMetadata(metadata)).extrinsic.version);
425
400
  }
426
401
 
427
402
  /** Internal seam so `import.meta.vitest` can stub the metadata decode. @internal */
@@ -705,30 +680,41 @@ if (import.meta.vitest) {
705
680
  const { test, expect, vi, describe } = import.meta.vitest;
706
681
 
707
682
  test("host signing prefers V4 (tx-ext version 0) on a dual V4/V5 runtime", () => {
708
- // txExtVersion 0 regardless of what the extension map lists.
709
- expect(selectHostTxExtVersion([4, 5], [0])).toBe(0);
683
+ expect(selectHostTxExtVersion([4, 5])).toBe(0);
710
684
  });
711
685
 
712
- test("host signing uses the general tx-ext version 5 when V4 is unavailable", () => {
713
- // The value comes from the extension-version map, not the format list.
714
- expect(selectHostTxExtVersion([5], [0, 5])).toBe(5);
686
+ test("host signing uses the V5 selector when the runtime offers no V4", () => {
687
+ expect(selectHostTxExtVersion([5])).toBe(5);
715
688
  });
716
689
 
717
690
  test("host signing maps a V4-only runtime to the wire sentinel", () => {
718
- expect(selectHostTxExtVersion([4], [0])).toBe(0);
691
+ expect(selectHostTxExtVersion([4])).toBe(0);
692
+ });
693
+
694
+ test("host signing prefers V5 over a format it does not know", () => {
695
+ // max(formats) would send 6, which no host accepts.
696
+ expect(selectHostTxExtVersion([5, 6])).toBe(5);
719
697
  });
720
698
 
721
- test("host signing does not confuse extrinsic format 5 with a tx-ext version", () => {
722
- // V5-only runtime that only supports tx-ext version 0 (no general
723
- // extension set): the old code returned 5 (the format number); now this
724
- // must throw rather than send an unsupported txExtVersion.
725
- expect(() => selectHostTxExtVersion([5], [0])).toThrow(/no.*version 5/i);
699
+ test("host signing rejects a runtime offering neither format 4 nor 5", () => {
700
+ expect(() => selectHostTxExtVersion([6])).toThrow(/no extrinsic format 4 or 5/i);
726
701
  });
727
702
 
728
703
  test("host signing rejects metadata with no extrinsic version", () => {
729
- expect(() => selectHostTxExtVersion([], [0])).toThrow(
730
- "No extrinsic version found in metadata",
731
- );
704
+ expect(() => selectHostTxExtVersion([])).toThrow("No extrinsic version found in metadata");
705
+ });
706
+
707
+ test("deriveTxExtVersion reads the format list out of every tracked chain's metadata", async () => {
708
+ const { readFileSync, readdirSync } = await import("node:fs");
709
+ const dir = new URL("../../descriptors/.papi/metadata/", import.meta.url);
710
+ const blobs = readdirSync(dir).filter((name) => name.endsWith(".scale"));
711
+
712
+ expect(blobs.length, "raise when a chain is added").toBeGreaterThanOrEqual(11);
713
+ // Every deployed runtime still offers format 4, so V4 wins. Fails the day one drops it.
714
+ for (const name of blobs) {
715
+ const metadata = new Uint8Array(readFileSync(new URL(name, dir)));
716
+ expect(deriveTxExtVersion(metadata), name).toBe(0);
717
+ }
732
718
  });
733
719
 
734
720
  /** Minimal fake of the truapi account/signing domains used to test the adapter. */
package/src/index.ts CHANGED
@@ -105,6 +105,10 @@ export type { DevicePermissionKind, RemotePermissionItem } from "./permissions.j
105
105
  export { getThemeProvider } from "./theme.js";
106
106
  export type { ThemeMode, ThemeName, ThemeProvider, ThemeVariant } from "./theme.js";
107
107
 
108
+ // Locale provider
109
+ export { getLocaleProvider } from "./locale.js";
110
+ export type { LocaleInfo, LocaleProvider } from "./locale.js";
111
+
108
112
  // Entropy derivation (RFC-0007)
109
113
  export { deriveEntropy } from "./entropy.js";
110
114
 
package/src/locale.ts ADDED
@@ -0,0 +1,76 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Higher-level wrapper for the host's locale subscription, backed by
5
+ * `truApi.locale.subscribe`.
6
+ *
7
+ * `getLocaleProvider` returns a handle whose `subscribeLocale(cb)` delivers a
8
+ * typed {@link LocaleInfo} — a `{ languageTag }` struct carrying a BCP 47 tag
9
+ * such as `"en"`, `"pt-BR"` or `"zh-Hans"` — and yields a
10
+ * {@link HostSubscription} (`unsubscribe` + `onInterrupt`).
11
+ *
12
+ * @module
13
+ */
14
+
15
+ import type { HostLocaleSubscribeItem, TrUApiClient } from "@parity/truapi";
16
+
17
+ import { getClient, subscribeWithInterrupt } from "./transport.js";
18
+ import type { HostSubscription } from "./types.js";
19
+
20
+ /**
21
+ * Host locale value. A `{ languageTag }` struct re-exported from
22
+ * `@parity/truapi`.
23
+ */
24
+ export type LocaleInfo = HostLocaleSubscribeItem;
25
+
26
+ /**
27
+ * Host locale provider handle. `subscribeLocale(callback)` receives a typed
28
+ * {@link LocaleInfo} on every change and returns a {@link HostSubscription}.
29
+ */
30
+ export interface LocaleProvider {
31
+ subscribeLocale(callback: (locale: LocaleInfo) => void): HostSubscription;
32
+ }
33
+
34
+ /** Build a {@link LocaleProvider} over a TruAPI client's `locale` domain. */
35
+ function adaptLocaleProvider(client: TrUApiClient): LocaleProvider {
36
+ return {
37
+ subscribeLocale(callback) {
38
+ return subscribeWithInterrupt(client.locale.subscribe(), callback);
39
+ },
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Get the host locale provider, backed by `truApi.locale.*`. Returns `null`
45
+ * when running outside a host container.
46
+ *
47
+ * The tag is whatever the host reports; a product that ships no catalog entry
48
+ * for it chooses its own fallback.
49
+ *
50
+ * @returns The locale provider, or `null` if unavailable.
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * import { getLocaleProvider } from "@parity/product-sdk-host";
55
+ *
56
+ * const provider = await getLocaleProvider();
57
+ * if (provider) {
58
+ * const sub = provider.subscribeLocale((locale) => {
59
+ * i18n.activate(SUPPORTED.has(locale.languageTag) ? locale.languageTag : "en");
60
+ * });
61
+ * // sub.unsubscribe() to stop listening
62
+ * }
63
+ * ```
64
+ */
65
+ export async function getLocaleProvider(): Promise<LocaleProvider | null> {
66
+ const client = await getClient();
67
+ return client ? adaptLocaleProvider(client) : null;
68
+ }
69
+
70
+ if (import.meta.vitest) {
71
+ const { test, expect } = import.meta.vitest;
72
+
73
+ test("getLocaleProvider returns null outside a container", async () => {
74
+ expect(await getLocaleProvider()).toBeNull();
75
+ });
76
+ }
package/src/testing.ts CHANGED
@@ -14,8 +14,9 @@
14
14
  * (and `getChainAPI()` on top of it) resolves in tests; see the `chainInfo`
15
15
  * option. Not modeled: the rest of the PAPI `chain` JSON-RPC surface behind
16
16
  * `getHostProvider()` — there's no chain-read fake, by design; the host owns RPC
17
- * selection — and the `chat` / `coinPayment` / `entropy` / `notifications` /
18
- * `payment` / `permissions` / `resourceAllocation` / `theme` domains. Touching
17
+ * selection — the `system` domain's `info` / `getProductContext`, and the
18
+ * `chat` / `coinPayment` / `entropy` / `locale` / `notifications` / `payment` /
19
+ * `permissions` / `resourceAllocation` / `theme` domains. Touching
19
20
  * an unmodeled domain throws a descriptive error rather than failing with
20
21
  * `undefined is not a function`.
21
22
  *
@@ -249,11 +250,12 @@ export function createFakeTruApiClient(options?: CreateFakeTruApiClientOptions):
249
250
  okAsync({ proof: { tag: "Sr25519", value: { signature, signer: publicKey } } }),
250
251
  submit: () => okAsync(undefined),
251
252
  },
252
- system: {
253
+ // `info` and `getProductContext` are not modeled; they still throw.
254
+ system: notModeled("system", {
253
255
  handshake: () => okAsync(undefined),
254
256
  featureSupported: () => okAsync({ supported: chainSupported }),
255
257
  navigateTo: () => okAsync(undefined),
256
- },
258
+ }),
257
259
  preimage: {
258
260
  lookupSubscribe: ({ request: { key } }) =>
259
261
  oneShotObservable({ value: preimages.get(key) }),
@@ -280,6 +282,7 @@ export function createFakeTruApiClient(options?: CreateFakeTruApiClientOptions):
280
282
  chat: notModeled("chat"),
281
283
  coinPayment: notModeled("coinPayment"),
282
284
  entropy: notModeled("entropy"),
285
+ locale: notModeled("locale"),
283
286
  notifications: notModeled("notifications"),
284
287
  payment: notModeled("payment"),
285
288
  permissions: notModeled("permissions"),