@parity/product-sdk-host 0.11.0 → 0.12.0

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/src/container.ts CHANGED
@@ -1,12 +1,23 @@
1
1
  // Copyright 2026 Parity Technologies (UK) Ltd.
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  import type { JsonRpcProvider } from "polkadot-api";
4
- import { createLogger } from "@parity/product-sdk-logger";
5
- import { enumValue, type Transport } from "@novasamatech/host-api";
4
+ import type { HexString, TrUApiClient } from "@parity/truapi";
6
5
 
6
+ import { formatHostError } from "./errors.js";
7
+ import { createHostPapiProvider } from "./papi-provider.js";
8
+ import { getClient, isCorrectEnvironment, subscribeWithInterrupt } from "./transport.js";
9
+ import { fromHex, toHex, unwrapHostResult } from "./truapi.js";
7
10
  import type { HostLocalStorage, HostStatementStore } from "./types.js";
8
11
 
9
- const log = createLogger("host:container");
12
+ const textEncoder = new TextEncoder();
13
+ const textDecoder = new TextDecoder();
14
+
15
+ /**
16
+ * Synchronous container detection — fast heuristic check (iframe, webview
17
+ * marker, or injected host message port). Re-exported from the transport
18
+ * bootstrap, which owns the detection logic.
19
+ */
20
+ export { isCorrectEnvironment as isInsideContainerSync } from "./transport.js";
10
21
 
11
22
  /**
12
23
  * Thrown by {@link getHostProvider} when the host container is reachable but does
@@ -33,35 +44,24 @@ export class ChainNotSupportedError extends Error {
33
44
  }
34
45
 
35
46
  /**
36
- * Ask the host whether it can serve the given chain, using the same
37
- * `host_feature_supported` check the wrapper's provider performs internally
38
- * before it decides whether to start a real provider or a no-op one.
47
+ * Ask the host whether it can serve the given chain, via
48
+ * `system.featureSupported({ tag: "Chain", })`. Gates {@link getHostProvider}
49
+ * the same way the upstream wrapper's provider gated itself internally before
50
+ * deciding whether to start a real provider or a no-op one.
39
51
  *
40
- * @throws If the host connection never becomes ready, or the host rejects the
41
- * support check outright. Both are non-hanging, catchable failures.
52
+ * @throws If the host rejects the support check outright a non-hanging,
53
+ * catchable failure.
42
54
  */
43
55
  async function isChainSupportedByHost(
44
- sdk: typeof import("@novasamatech/host-api-wrapper"),
45
- genesisHash: `0x${string}`,
56
+ client: TrUApiClient,
57
+ genesisHash: HexString,
46
58
  ): Promise<boolean> {
47
- const ready = await sdk.sandboxTransport.isReady();
48
- if (!ready) {
49
- throw new Error(
50
- `Host connection did not become ready; cannot verify support for chain ${genesisHash}.`,
51
- );
52
- }
53
- const result = await sdk.hostApi.featureSupported(
54
- enumValue("v1", enumValue("Chain", genesisHash)),
55
- );
56
- return result.match(
57
- (ok) => ok.value === true,
58
- (err) => {
59
- // The reason lives at value.payload.reason for host-protocol errors and
60
- // value.reason for request-level ones; tolerate both against upstream drift.
61
- const value = (err as { value?: { payload?: { reason?: string }; reason?: string } })
62
- ?.value;
63
- const reason = value?.payload?.reason ?? value?.reason ?? "unknown reason";
64
- throw new Error(`Host rejected the chain-support check for ${genesisHash}: ${reason}`);
59
+ return client.system.featureSupported({ tag: "Chain", value: { genesisHash } }).match(
60
+ (response) => response.supported,
61
+ (error) => {
62
+ throw new Error(
63
+ `Host rejected the chain-support check for ${genesisHash}: ${formatHostError(error)}`,
64
+ );
65
65
  },
66
66
  );
67
67
  }
@@ -71,257 +71,194 @@ async function isChainSupportedByHost(
71
71
  *
72
72
  * The SDK is designed to run exclusively inside a host container. This function
73
73
  * is primarily useful for early validation or informational purposes.
74
- *
75
- * Uses product-sdk's sandboxProvider as primary detection.
76
- * Falls back to manual signal checks when product-sdk is not installed.
77
74
  */
78
75
  export async function isInsideContainer(): Promise<boolean> {
79
- if (typeof window === "undefined") return false;
76
+ return isCorrectEnvironment();
77
+ }
80
78
 
81
- try {
82
- const sdk = await import("@novasamatech/host-api-wrapper");
83
- return sdk.sandboxProvider.isCorrectEnvironment();
84
- } catch {
85
- return isInsideContainerSync();
79
+ /**
80
+ * Adapt the TruAPI client's raw `localStorage` domain (hex-encoded
81
+ * `read`/`write`/`clear`) into the richer {@link HostLocalStorage} surface that
82
+ * the Storage package's `KvStore` and other consumers expect.
83
+ */
84
+ function adaptLocalStorage(client: TrUApiClient): HostLocalStorage {
85
+ const ls = client.localStorage;
86
+
87
+ async function readBytes(key: string): Promise<Uint8Array | undefined> {
88
+ const response = await unwrapHostResult(ls.read({ key }), "host localStorage read failed");
89
+ return response.value !== undefined ? fromHex(response.value) : undefined;
86
90
  }
91
+
92
+ async function writeBytes(key: string, value: Uint8Array): Promise<void> {
93
+ await unwrapHostResult(
94
+ ls.write({ key, value: toHex(value) }),
95
+ "host localStorage write failed",
96
+ );
97
+ }
98
+
99
+ async function readString(key: string): Promise<string> {
100
+ const bytes = await readBytes(key);
101
+ return bytes ? textDecoder.decode(bytes) : "";
102
+ }
103
+
104
+ async function writeString(key: string, value: string): Promise<void> {
105
+ return writeBytes(key, textEncoder.encode(value));
106
+ }
107
+
108
+ async function readJSON(key: string): Promise<unknown> {
109
+ const text = await readString(key);
110
+ return text ? JSON.parse(text) : null;
111
+ }
112
+
113
+ async function writeJSON(key: string, value: unknown): Promise<void> {
114
+ return writeString(key, JSON.stringify(value));
115
+ }
116
+
117
+ async function clear(key: string): Promise<void> {
118
+ await unwrapHostResult(ls.clear({ key }), "host localStorage clear failed");
119
+ }
120
+
121
+ return { readString, writeString, readJSON, writeJSON, readBytes, writeBytes, clear };
87
122
  }
88
123
 
89
124
  /**
90
125
  * Get the Host API localStorage instance when running inside a container.
91
- * Returns null outside a container or when product-sdk is unavailable.
126
+ * Returns null outside a container or when the host transport is unavailable.
92
127
  */
93
128
  export async function getHostLocalStorage(): Promise<HostLocalStorage | null> {
94
- if (!(await isInsideContainer())) return null;
95
-
96
- try {
97
- const sdk = await import("@novasamatech/host-api-wrapper");
98
- return sdk.hostLocalStorage as HostLocalStorage;
99
- } catch (err) {
100
- log.debug("getHostLocalStorage unavailable", err);
101
- return null;
102
- }
129
+ const client = await getClient();
130
+ return client ? adaptLocalStorage(client) : null;
103
131
  }
104
132
 
105
133
  /**
106
- * Construct a fresh host-backed `HostLocalStorage` instance with an optional
107
- * custom transport. Use this when you need a non-default transport (e.g.
108
- * for tests); otherwise prefer {@link getHostLocalStorage}, which returns
109
- * the shared singleton.
134
+ * Construct a host-backed `HostLocalStorage` instance. Retained for API
135
+ * compatibility; with the single cached TruAPI client this is equivalent to
136
+ * {@link getHostLocalStorage}.
110
137
  *
111
- * Mirrors `createLocalStorage` from `@novasamatech/host-api-wrapper`.
112
- *
113
- * @param transport - Optional transport; defaults to the sandbox transport.
114
- * @returns A new `HostLocalStorage` instance, or `null` if unavailable.
138
+ * @returns A `HostLocalStorage` instance, or `null` if unavailable.
115
139
  */
116
- export async function createHostLocalStorage(
117
- transport?: Transport,
118
- ): Promise<HostLocalStorage | null> {
119
- if (!(await isInsideContainer())) return null;
120
-
121
- try {
122
- const sdk = await import("@novasamatech/host-api-wrapper");
123
- return sdk.createLocalStorage(transport);
124
- } catch (err) {
125
- log.debug("createHostLocalStorage unavailable", err);
126
- return null;
127
- }
140
+ export async function createHostLocalStorage(): Promise<HostLocalStorage | null> {
141
+ return getHostLocalStorage();
128
142
  }
129
143
 
130
144
  /**
131
145
  * Get a PAPI-compatible JSON-RPC provider that routes through the host connection.
132
146
  *
133
- * When running inside a Polkadot container, this wraps the chain connection via the
134
- * host's `createPapiProvider`, enabling shared connections and efficient routing.
135
- * Returns `null` when `@novasamatech/host-api-wrapper` is unavailable or when not
136
- * running inside a container.
147
+ * When running inside a Polkadot container, this builds a `JsonRpcProvider` over
148
+ * `truApi.chain.*` (see {@link module:papi-provider}), enabling shared
149
+ * connections and efficient routing. Returns `null` when not running inside a
150
+ * container.
137
151
  *
138
152
  * @param genesisHash - Genesis hash of the target chain (`0x`-prefixed hex string).
139
153
  * @returns A host-routed `JsonRpcProvider`, or `null` if unavailable.
140
154
  * @throws {ChainNotSupportedError} When inside a container but the host can't serve
141
155
  * the chain — surfaced instead of returning a provider that would hang forever.
142
156
  */
143
- export async function getHostProvider(genesisHash: `0x${string}`): Promise<JsonRpcProvider | null> {
144
- let sdk: typeof import("@novasamatech/host-api-wrapper");
145
- try {
146
- sdk = await import("@novasamatech/host-api-wrapper");
147
- } catch (err) {
148
- // Wrapper not installed — we're not running inside a container.
149
- log.debug("getHostProvider unavailable", err);
150
- return null;
151
- }
152
- return resolveHostProvider(sdk, genesisHash);
157
+ export async function getHostProvider(genesisHash: HexString): Promise<JsonRpcProvider | null> {
158
+ const client = await getClient();
159
+ if (!client) return null;
160
+ return resolveHostProvider(client, genesisHash);
153
161
  }
154
162
 
155
163
  /**
156
- * Decide whether to build a host provider for `genesisHash`, given the resolved
157
- * wrapper module. Split out of {@link getHostProvider} so the decision logic can
158
- * be unit-tested with a fake wrapper, without re-importing the real
159
- * (browser-only) module.
164
+ * Decide whether to build a host provider for `genesisHash`, given a ready
165
+ * TruAPI client. Split out of {@link getHostProvider} so the decision logic can
166
+ * be unit-tested with a fake client.
160
167
  *
161
- * @returns the provider, or `null` when not inside a container.
168
+ * @returns the provider.
162
169
  * @throws {ChainNotSupportedError} when the host can't serve the chain.
163
170
  */
164
171
  async function resolveHostProvider(
165
- sdk: typeof import("@novasamatech/host-api-wrapper"),
166
- genesisHash: `0x${string}`,
167
- ): Promise<JsonRpcProvider | null> {
168
- // Outside a host container there is no provider to hand back. Mirrors
169
- // createPapiProvider's own environment guard; callers treat null as
170
- // "not inside a container".
171
- if (!sdk.sandboxTransport.isCorrectEnvironment()) {
172
- return null;
173
- }
174
-
175
- // Inside a container: confirm the host can actually serve this chain before
176
- // handing PAPI a provider. When the host doesn't support the chain, the
177
- // wrapper's fallback provider silently swallows every JSON-RPC request and
178
- // the caller hangs forever with no rejection. Surface a catchable error.
179
- if (!(await isChainSupportedByHost(sdk, genesisHash))) {
172
+ client: TrUApiClient,
173
+ genesisHash: HexString,
174
+ ): Promise<JsonRpcProvider> {
175
+ // Confirm the host can actually serve this chain before handing PAPI a
176
+ // provider. When the host doesn't support the chain, a provider would silently
177
+ // swallow every JSON-RPC request and the caller hangs forever with no
178
+ // rejection. Surface a catchable error instead.
179
+ if (!(await isChainSupportedByHost(client, genesisHash))) {
180
180
  throw new ChainNotSupportedError(genesisHash);
181
181
  }
182
182
 
183
- return sdk.createPapiProvider(genesisHash);
183
+ return createHostPapiProvider(client, genesisHash);
184
184
  }
185
185
 
186
- /**
187
- * Synchronous container detection — fast heuristic check without product-sdk.
188
- *
189
- * Checks for iframe, webview marker, and host message port signals.
190
- * Use this when you need a quick sync check (e.g., in hot code paths).
191
- * For full detection including product-sdk, use {@link isInsideContainer} (async).
192
- */
193
- export function isInsideContainerSync(): boolean {
194
- if (typeof window === "undefined") return false;
195
-
196
- const win = window as unknown as Record<string, unknown>;
197
-
198
- // Iframe detection (polkadot.com browser)
199
- try {
200
- if (window !== window.top) return true;
201
- } catch {
202
- // Cross-origin iframe — likely inside a container
203
- return true;
204
- }
205
-
206
- // Webview detection (Polkadot Desktop)
207
- if (win.__HOST_WEBVIEW_MARK__ === true) return true;
208
-
209
- // Desktop message-passing API
210
- if (win.__HOST_API_PORT__ != null) return true;
211
-
212
- return false;
186
+ /** Build a {@link HostStatementStore} over a TruAPI client's `statementStore` domain. */
187
+ function adaptStatementStore(client: TrUApiClient): HostStatementStore {
188
+ const ss = client.statementStore;
189
+ return {
190
+ subscribe(filter, callback) {
191
+ const request =
192
+ "matchAll" in filter
193
+ ? ({ tag: "MatchAll", value: filter.matchAll } as const)
194
+ : ({ tag: "MatchAny", value: filter.matchAny } as const);
195
+ // `RemoteStatementStoreSubscribeItem` is structurally a StatementsPage.
196
+ return subscribeWithInterrupt(ss.subscribe({ request }), callback);
197
+ },
198
+ async createProofAuthorized(statement) {
199
+ const response = await unwrapHostResult(
200
+ ss.createProofAuthorized(statement),
201
+ "createProofAuthorized failed",
202
+ );
203
+ return response.proof;
204
+ },
205
+ async submit(signedStatement) {
206
+ await unwrapHostResult(ss.submit(signedStatement), "statement submit failed");
207
+ },
208
+ };
213
209
  }
214
210
 
215
211
  /**
216
- * Get the host API statement store when running inside a container.
212
+ * Get the host statement store when running inside a container, backed by
213
+ * `truApi.statementStore.*`.
217
214
  *
218
- * Returns a statement store with `subscribe`, `createProof`, and `submit` methods
219
- * that communicate through the host's native binary protocol — bypassing JSON-RPC
220
- * entirely. Returns `null` when `@novasamatech/host-api-wrapper` is unavailable.
215
+ * Returns a store with `subscribe`, `createProofAuthorized`, and `submit` that
216
+ * communicate through the host's native binary protocol — bypassing JSON-RPC
217
+ * entirely. Returns `null` outside a host container.
221
218
  *
222
219
  * @returns The host statement store, or `null` if unavailable.
223
220
  */
224
221
  export async function getStatementStore(): Promise<HostStatementStore | null> {
225
- try {
226
- const sdk = await import("@novasamatech/host-api-wrapper");
227
- return sdk.createStatementStore() as HostStatementStore;
228
- } catch (err) {
229
- log.debug("getStatementStore unavailable", err);
230
- return null;
231
- }
222
+ const client = await getClient();
223
+ return client ? adaptStatementStore(client) : null;
232
224
  }
233
225
 
234
226
  if (import.meta.vitest) {
235
- const { test, expect, vi } = import.meta.vitest;
236
-
237
- // A self-contained stand-in for the host wrapper, so the chain-support
238
- // decision can be tested without re-importing the real (browser-only) module.
239
- const fakeProvider = (() => {}) as unknown as JsonRpcProvider;
240
- function makeFakeSdk(opts: {
241
- inContainer?: boolean;
242
- ready?: boolean;
243
- supported?: boolean;
244
- featureErr?: string | null;
245
- onCreate?: (genesisHash: string) => void;
246
- }) {
247
- const { inContainer = true, ready = true, supported = true, featureErr = null } = opts;
227
+ const { test, expect, vi, afterEach } = import.meta.vitest;
228
+
229
+ afterEach(() => {
230
+ vi.unstubAllGlobals();
231
+ });
232
+
233
+ // A self-contained fake TruAPI client exposing just the `system` and `chain`
234
+ // domains the provider gate touches, so the chain-support decision can be
235
+ // tested without a real host connection.
236
+ function makeFakeClient(opts: { supported?: boolean; featureErr?: string | null }) {
237
+ const { supported = true, featureErr = null } = opts;
248
238
  return {
249
- sandboxTransport: {
250
- isCorrectEnvironment: () => inContainer,
251
- isReady: async () => ready,
252
- },
253
- hostApi: {
254
- featureSupported: (_payload: unknown) => ({
239
+ system: {
240
+ featureSupported: (_request: unknown) => ({
255
241
  match: (
256
- okFn: (ok: { tag: string; value: boolean }) => boolean,
257
- errFn: (err: { value: { payload: { reason: string } } }) => boolean,
258
- ) =>
259
- featureErr
260
- ? errFn({ value: { payload: { reason: featureErr } } })
261
- : okFn({ tag: "v1", value: supported }),
242
+ okFn: (ok: { supported: boolean }) => boolean,
243
+ errFn: (err: { reason: string }) => boolean,
244
+ ) => (featureErr ? errFn({ reason: featureErr }) : okFn({ supported })),
262
245
  }),
263
246
  },
264
- createPapiProvider: (genesisHash: string) => {
265
- opts.onCreate?.(genesisHash);
266
- return fakeProvider;
267
- },
268
- } as unknown as typeof import("@novasamatech/host-api-wrapper");
247
+ // Read synchronously by createHostPapiProvider; never invoked here.
248
+ chain: {},
249
+ } as unknown as TrUApiClient;
269
250
  }
270
251
 
271
- test("returns false in Node environment (no window)", async () => {
252
+ test("isInsideContainer is false in a Node environment (no window)", async () => {
272
253
  expect(await isInsideContainer()).toBe(false);
273
254
  });
274
255
 
275
- test("manualDetection returns true for __HOST_WEBVIEW_MARK__", async () => {
276
- const fakeWindow = {
277
- top: null,
278
- __HOST_WEBVIEW_MARK__: true,
279
- };
280
- vi.stubGlobal("window", fakeWindow);
281
- const result = await isInsideContainer();
282
- expect(result).toBe(true);
283
- vi.unstubAllGlobals();
284
- });
285
-
286
- test("manualDetection returns true for __HOST_API_PORT__", async () => {
287
- const fakeWindow = {
288
- top: null,
289
- __HOST_API_PORT__: 12345,
290
- };
291
- vi.stubGlobal("window", fakeWindow);
292
- const result = await isInsideContainer();
293
- expect(result).toBe(true);
294
- vi.unstubAllGlobals();
295
- });
296
-
297
- test("manualDetection returns false when no signals present", async () => {
298
- const fakeWindow = { top: null };
299
- Object.defineProperty(fakeWindow, "top", { get: () => fakeWindow });
300
- vi.stubGlobal("window", fakeWindow);
301
- const result = await isInsideContainer();
302
- expect(result).toBe(false);
303
- vi.unstubAllGlobals();
304
- });
305
-
306
- test("manualDetection returns true for cross-origin iframe", async () => {
307
- const fakeWindow = {};
308
- Object.defineProperty(fakeWindow, "top", {
309
- get: () => {
310
- throw new DOMException("cross-origin");
311
- },
312
- });
313
- vi.stubGlobal("window", fakeWindow);
314
- const result = await isInsideContainer();
315
- expect(result).toBe(true);
316
- vi.unstubAllGlobals();
317
- });
318
-
319
- test("manualDetection returns true when window !== window.top (iframe)", async () => {
320
- const fakeWindow = { top: {} }; // top is a different object
321
- vi.stubGlobal("window", fakeWindow);
322
- const result = await isInsideContainer();
323
- expect(result).toBe(true);
324
- vi.unstubAllGlobals();
256
+ test("isInsideContainer detects an injected host port", async () => {
257
+ const win = {};
258
+ Object.defineProperty(win, "top", { get: () => win });
259
+ (win as Record<string, unknown>).__HOST_API_PORT__ = 12345;
260
+ vi.stubGlobal("window", win);
261
+ expect(await isInsideContainer()).toBe(true);
325
262
  });
326
263
 
327
264
  test("getHostLocalStorage returns null outside container", async () => {
@@ -332,43 +269,71 @@ if (import.meta.vitest) {
332
269
  expect(await createHostLocalStorage()).toBeNull();
333
270
  });
334
271
 
335
- // --- chain-support gating (resolveHostProvider) ---
272
+ test("adaptLocalStorage round-trips strings, JSON, and bytes over the TruAPI client", async () => {
273
+ // Minimal in-memory fake of the TruAPI localStorage domain (hex values).
274
+ const store = new Map<string, `0x${string}`>();
275
+ const okAsync = <T>(value: T) => ({
276
+ match: async (onOk: (v: T) => unknown) => onOk(value),
277
+ });
278
+ const fakeClient = {
279
+ localStorage: {
280
+ read: ({ key }: { key: string }) => okAsync({ value: store.get(key) }),
281
+ write: ({ key, value }: { key: string; value: `0x${string}` }) => {
282
+ store.set(key, value);
283
+ return okAsync(undefined);
284
+ },
285
+ clear: ({ key }: { key: string }) => {
286
+ store.delete(key);
287
+ return okAsync(undefined);
288
+ },
289
+ },
290
+ } as unknown as TrUApiClient;
291
+
292
+ const ls = adaptLocalStorage(fakeClient);
293
+ expect(await ls.readString("missing")).toBe("");
294
+ expect(await ls.readJSON("missing")).toBeNull();
295
+ expect(await ls.readBytes("missing")).toBeUndefined();
336
296
 
337
- test("resolves to the provider when supported, and null outside a container", async () => {
338
- const created: string[] = [];
339
- const onCreate = (g: string) => created.push(g);
297
+ await ls.writeString("s", "hello");
298
+ expect(await ls.readString("s")).toBe("hello");
340
299
 
341
- // Inside a container, supported chain -> real provider.
342
- expect(await resolveHostProvider(makeFakeSdk({ onCreate }), "0xabc")).toBe(fakeProvider);
343
- // Outside a container -> null, without constructing a provider.
344
- expect(
345
- await resolveHostProvider(makeFakeSdk({ inContainer: false, onCreate }), "0xdef"),
346
- ).toBeNull();
300
+ await ls.writeJSON("j", { a: 1 });
301
+ expect(await ls.readJSON("j")).toEqual({ a: 1 });
347
302
 
348
- expect(created).toEqual(["0xabc"]);
303
+ await ls.writeBytes("b", new Uint8Array([1, 2, 3]));
304
+ expect(Array.from((await ls.readBytes("b")) ?? [])).toEqual([1, 2, 3]);
305
+
306
+ await ls.clear("s");
307
+ expect(await ls.readString("s")).toBe("");
349
308
  });
350
309
 
351
- test.each([
352
- { when: "the host doesn't support the chain", opts: { supported: false } },
353
- { when: "the host connection never becomes ready", opts: { ready: false } },
354
- ])("throws (and never builds a provider) when $when", async ({ opts }) => {
355
- const created: string[] = [];
356
- const sdk = makeFakeSdk({ ...opts, onCreate: (g) => created.push(g) });
357
- await expect(resolveHostProvider(sdk, "0xabc")).rejects.toThrow();
358
- // Crucially: no provider is created, so PAPI never receives a hanging no-op.
359
- expect(created).toEqual([]);
310
+ // --- chain-support gating (resolveHostProvider over truApi.system/chain) ---
311
+
312
+ test("resolves to a provider when the host supports the chain", async () => {
313
+ const provider = await resolveHostProvider(makeFakeClient({ supported: true }), "0xabc");
314
+ // createHostPapiProvider returns a JsonRpcProvider (an onMessage -> connection fn).
315
+ expect(typeof provider).toBe("function");
360
316
  });
361
317
 
362
- test("unsupported chains throw a ChainNotSupportedError carrying the genesis hash", async () => {
363
- const err = await resolveHostProvider(makeFakeSdk({ supported: false }), "0xfeed").catch(
318
+ test("throws ChainNotSupportedError when the host doesn't support the chain", async () => {
319
+ const err = await resolveHostProvider(makeFakeClient({ supported: false }), "0xfeed").catch(
364
320
  (e) => e,
365
321
  );
366
322
  expect(err).toBeInstanceOf(ChainNotSupportedError);
367
323
  expect((err as ChainNotSupportedError).genesisHash).toBe("0xfeed");
368
324
  });
369
325
 
370
- test("getStatementStore returns null when product-sdk unavailable", async () => {
371
- const result = await getStatementStore();
372
- expect(result).toBeNull();
326
+ test("throws when the host rejects the support check", async () => {
327
+ await expect(
328
+ resolveHostProvider(makeFakeClient({ featureErr: "boom" }), "0xabc"),
329
+ ).rejects.toThrow(/boom/);
330
+ });
331
+
332
+ test("getHostProvider returns null outside a container", async () => {
333
+ expect(await getHostProvider("0xabc")).toBeNull();
334
+ });
335
+
336
+ test("getStatementStore returns null outside a container", async () => {
337
+ expect(await getStatementStore()).toBeNull();
373
338
  });
374
339
  }