@parity/product-sdk-host 0.16.0 → 0.18.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.
@@ -1,12 +1,14 @@
1
1
  import { TrUApiClient, ChainIdentifier, HexString } from '@parity/truapi';
2
+ import { ConnectionStatus } from '@parity/truapi/sandbox';
2
3
 
3
4
  /**
4
5
  * Access to the in-house TruAPI client (`@parity/truapi`) for the host package.
5
6
  *
6
- * Environment detection and the lazily-built, cached client come from
7
- * `@parity/truapi/sandbox`; this module layers the product-sdk-specific glue on
8
- * top — an async {@link getClient} accessor and {@link subscribeWithInterrupt},
9
- * which adapts a truapi stream into the host's {@link HostSubscription} shape.
7
+ * Environment detection, the lazily-built cached client, and the connection-status
8
+ * signal come from `@parity/truapi/sandbox`; this module layers the
9
+ * product-sdk-specific glue on top — an async {@link getClient} accessor,
10
+ * {@link subscribeConnectionStatus}, and {@link subscribeWithInterrupt}, which
11
+ * adapts a truapi stream into the host's {@link HostSubscription} shape.
10
12
  *
11
13
  * @module
12
14
  */
@@ -20,6 +22,9 @@ import { TrUApiClient, ChainIdentifier, HexString } from '@parity/truapi';
20
22
  * Calling this in a production build silently reroutes every host accessor to
21
23
  * the injected client, so we warn — it almost always means a `/testing` import
22
24
  * leaked into a production path.
25
+ *
26
+ * Injecting or clearing notifies {@link subscribeConnectionStatus} subscribers,
27
+ * so a product's "host lost" path can be exercised by disposing the fake host.
23
28
  */
24
29
  declare function setTruApiClient(client: TrUApiClient | null): void;
25
30
  /**
@@ -27,6 +32,38 @@ declare function setTruApiClient(client: TrUApiClient | null): void;
27
32
  * sandbox heuristic (iframe / webview marker / injected message port).
28
33
  */
29
34
  declare function isCorrectEnvironment(): boolean;
35
+ /**
36
+ * Connection lifecycle of the host channel: `"connecting"` while the client waits
37
+ * for the host, `"connected"` once the channel is established, `"disconnected"`
38
+ * outside a host container or after the channel closes.
39
+ *
40
+ * Not the same concept as `@parity/product-sdk-signer`'s identically-shaped
41
+ * `ConnectionStatus`, which tracks a signer provider rather than the transport.
42
+ */
43
+ type HostConnectionStatus = ConnectionStatus;
44
+ /**
45
+ * Test-only: push `status` to every {@link subscribeConnectionStatus} subscriber,
46
+ * so a product can exercise its reconnecting / offline UI. The host-side
47
+ * counterpart of `@parity/product-sdk-signer`'s `FakeSignerProvider.emitStatus`.
48
+ * Exposed through `@parity/product-sdk-host/testing`, not the main entry.
49
+ */
50
+ declare function emitConnectionStatus(status: HostConnectionStatus): void;
51
+ /**
52
+ * Subscribe to host-channel connection status. The callback fires synchronously
53
+ * with the current status and again on every change; the returned function
54
+ * unsubscribes. Repeats of the status you already have are suppressed.
55
+ *
56
+ * This is the **transport** channel. For the host's account-level connection —
57
+ * what drives `@parity/product-sdk-signer`'s `ConnectionStatus` — use
58
+ * `AccountsProvider.subscribeAccountConnectionStatus` instead.
59
+ *
60
+ * Subscribing is not passive: outside an established channel the first subscribe
61
+ * builds the client and provider, so this can be what constructs the transport.
62
+ *
63
+ * Honours the `setTruApiClient` seam — an injected client is connected by
64
+ * definition, and injecting or clearing one notifies live subscribers.
65
+ */
66
+ declare function subscribeConnectionStatus(callback: (status: HostConnectionStatus) => void): () => void;
30
67
 
31
68
  /**
32
69
  * Host chain discovery. Resolves chain roles to genesis hashes against the
@@ -67,4 +104,4 @@ interface HostChainDiscovery {
67
104
  */
68
105
  declare function getHostChainInfo(identifiers: readonly HostChainIdentifier[]): Promise<HostChainDiscovery | null>;
69
106
 
70
- export { type HostChainDiscovery as H, type HostChainIdentifier as a, getHostChainInfo as g, isCorrectEnvironment as i, setTruApiClient as s };
107
+ export { type HostChainDiscovery as H, type HostChainIdentifier as a, type HostConnectionStatus as b, setTruApiClient as c, emitConnectionStatus as e, getHostChainInfo as g, isCorrectEnvironment as i, subscribeConnectionStatus as s };
@@ -0,0 +1,89 @@
1
+ import { isCorrectEnvironment as isCorrectEnvironment$1, subscribeConnectionStatus as subscribeConnectionStatus$1, getClientSync as getClientSync$1 } from '@parity/truapi/sandbox';
2
+
3
+ // src/transport.ts
4
+ var clientOverride = null;
5
+ var localStatusListeners = /* @__PURE__ */ new Set();
6
+ function isProductionBuild() {
7
+ try {
8
+ return process.env.NODE_ENV === "production";
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+ function setTruApiClient(client) {
14
+ if (client !== null && isProductionBuild()) {
15
+ console.warn(
16
+ "[product-sdk] setTruApiClient() was called in a production build. This is a test-only seam from @parity/product-sdk-host/testing; a leaked import will silently reroute all host access to the injected client."
17
+ );
18
+ }
19
+ const wasOverridden = clientOverride !== null;
20
+ clientOverride = client;
21
+ if (wasOverridden !== (client !== null)) {
22
+ notifyLocalStatusListeners(client !== null ? "connected" : "disconnected");
23
+ }
24
+ }
25
+ function getClientSync() {
26
+ return clientOverride ?? getClientSync$1();
27
+ }
28
+ function isCorrectEnvironment() {
29
+ return clientOverride !== null || isCorrectEnvironment$1();
30
+ }
31
+ async function getClient() {
32
+ return getClientSync();
33
+ }
34
+ function latchDisconnected(previous, next) {
35
+ return next === "connecting" && previous === "disconnected" ? "disconnected" : next;
36
+ }
37
+ function notifyLocalStatusListeners(status) {
38
+ for (const listener of [...localStatusListeners]) listener(status);
39
+ }
40
+ function emitConnectionStatus(status) {
41
+ if (isProductionBuild()) {
42
+ console.warn(
43
+ "[product-sdk] emitConnectionStatus() was called in a production build. This is a test-only seam from @parity/product-sdk-host/testing; a leaked import will report a fabricated connection status to real subscribers."
44
+ );
45
+ }
46
+ notifyLocalStatusListeners(status);
47
+ }
48
+ function subscribeConnectionStatus(callback) {
49
+ let last = null;
50
+ const deliver = (status, fromSandbox) => {
51
+ const next = fromSandbox ? latchDisconnected(last, status) : status;
52
+ if (next === last) return;
53
+ last = next;
54
+ callback(next);
55
+ };
56
+ const onLocal = (status) => deliver(status, false);
57
+ localStatusListeners.add(onLocal);
58
+ if (clientOverride !== null) {
59
+ onLocal("connected");
60
+ return () => void localStatusListeners.delete(onLocal);
61
+ }
62
+ const unsubscribeSandbox = subscribeConnectionStatus$1((status) => deliver(status, true));
63
+ return () => {
64
+ localStatusListeners.delete(onLocal);
65
+ unsubscribeSandbox();
66
+ };
67
+ }
68
+ function subscribeWithInterrupt(observable, onNext) {
69
+ let interruptCallback;
70
+ const sub = observable.subscribe({
71
+ next: onNext,
72
+ error: (reason) => interruptCallback?.(reason),
73
+ complete: () => interruptCallback?.()
74
+ });
75
+ return {
76
+ subscriptionId: sub.subscriptionId,
77
+ unsubscribe: () => sub.unsubscribe(),
78
+ onInterrupt: (callback) => {
79
+ interruptCallback = callback;
80
+ return () => {
81
+ if (interruptCallback === callback) interruptCallback = void 0;
82
+ };
83
+ }
84
+ };
85
+ }
86
+
87
+ export { emitConnectionStatus, getClient, isCorrectEnvironment, setTruApiClient, subscribeConnectionStatus, subscribeWithInterrupt };
88
+ //# sourceMappingURL=chunk-PAKEUP2Q.js.map
89
+ //# sourceMappingURL=chunk-PAKEUP2Q.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/transport.ts"],"names":["sandboxGetClientSync","sandboxIsCorrectEnvironment","sandboxSubscribeConnectionStatus"],"mappings":"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","file":"chunk-PAKEUP2Q.js","sourcesContent":["// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Access to the in-house TruAPI client (`@parity/truapi`) for the host package.\n *\n * Environment detection, the lazily-built cached client, and the connection-status\n * signal come from `@parity/truapi/sandbox`; this module layers the\n * product-sdk-specific glue on top — an async {@link getClient} accessor,\n * {@link subscribeConnectionStatus}, and {@link subscribeWithInterrupt}, which\n * adapts a truapi stream into the host's {@link HostSubscription} shape.\n *\n * @module\n */\n\nimport type { ObservableLike, TrUApiClient } from \"@parity/truapi\";\nimport {\n type ConnectionStatus,\n getClientSync as sandboxGetClientSync,\n isCorrectEnvironment as sandboxIsCorrectEnvironment,\n subscribeConnectionStatus as sandboxSubscribeConnectionStatus,\n} from \"@parity/truapi/sandbox\";\n\nimport type { HostSubscription } from \"./types.js\";\n\n/** A {@link HostSubscription} carrying the transport-assigned subscription id. */\nexport interface TransportSubscription extends HostSubscription {\n readonly subscriptionId: string;\n}\n\n// Test-only override. When set — via `setTruApiClient`, exposed through\n// `@parity/product-sdk-host/testing` — every host accessor resolves this client\n// instead of the sandbox one. `null` in production, so the branches below are\n// no-ops there.\nlet clientOverride: TrUApiClient | null = null;\n\n// Status subscribers registered here rather than only in the sandbox, so that\n// flipping the test seam is an *event*. The sandbox tracks only the client it\n// built itself, so it cannot know an injected client appeared or went away.\nconst localStatusListeners = new Set<(status: HostConnectionStatus) => void>();\n\nfunction isProductionBuild(): boolean {\n try {\n // Must stay a plain `process.env.NODE_ENV` member expression: bundlers\n // (Vite, esbuild, webpack) substitute it textually, which is how this\n // check works in browser builds where `process` doesn't exist.\n return process.env.NODE_ENV === \"production\";\n } catch {\n // No `process` and no bundler define — can't tell, stay quiet.\n return false;\n }\n}\n\n/**\n * Test-only seam: force {@link getClient} / {@link getClientSync} to return\n * `client`, and {@link isCorrectEnvironment} to report `true`. Pass `null` to\n * restore normal detection. Exposed through `@parity/product-sdk-host/testing`,\n * not the package's main entry.\n *\n * Calling this in a production build silently reroutes every host accessor to\n * the injected client, so we warn — it almost always means a `/testing` import\n * leaked into a production path.\n *\n * Injecting or clearing notifies {@link subscribeConnectionStatus} subscribers,\n * so a product's \"host lost\" path can be exercised by disposing the fake host.\n */\nexport function setTruApiClient(client: TrUApiClient | null): void {\n if (client !== null && isProductionBuild()) {\n console.warn(\n \"[product-sdk] setTruApiClient() was called in a production build. This is a test-only seam from @parity/product-sdk-host/testing; a leaked import will silently reroute all host access to the injected client.\",\n );\n }\n const wasOverridden = clientOverride !== null;\n clientOverride = client;\n if (wasOverridden !== (client !== null)) {\n notifyLocalStatusListeners(client !== null ? \"connected\" : \"disconnected\");\n }\n}\n\n/**\n * Synchronous TruAPI client accessor. Returns the injected test client when one\n * is set, otherwise the sandbox client (`null` outside a host container).\n */\nexport function getClientSync(): TrUApiClient | null {\n return clientOverride ?? sandboxGetClientSync();\n}\n\n/**\n * Host-container detection. `true` when a test client is injected, otherwise the\n * sandbox heuristic (iframe / webview marker / injected message port).\n */\nexport function isCorrectEnvironment(): boolean {\n return clientOverride !== null || sandboxIsCorrectEnvironment();\n}\n\n/**\n * Get the TruAPI client. Returns `null` outside a host container. Async wrapper\n * over {@link getClientSync} for the host wrappers that already `await` it.\n */\nexport async function getClient(): Promise<TrUApiClient | null> {\n return getClientSync();\n}\n\n/**\n * Connection lifecycle of the host channel: `\"connecting\"` while the client waits\n * for the host, `\"connected\"` once the channel is established, `\"disconnected\"`\n * outside a host container or after the channel closes.\n *\n * Not the same concept as `@parity/product-sdk-signer`'s identically-shaped\n * `ConnectionStatus`, which tracks a signer provider rather than the transport.\n */\nexport type HostConnectionStatus = ConnectionStatus;\n\n/**\n * Correct one defect in the sandbox's status signal: `@parity/truapi` never clears\n * its cached client when the pipe closes, so a subscriber arriving after a\n * disconnect re-derives `\"connecting\"` from the dead client — and because the\n * sandbox fans every change out to all listeners, that rewrites everyone's state\n * with no way back. Hold `\"disconnected\"` until a real `\"connected\"` arrives.\n *\n * Applies to sandbox-sourced statuses only. A status pushed by the test seam is\n * deliberate and passes through, so a fake host can still drive a reconnect.\n *\n * Outstanding upstream, not tied to the version we happen to be on: `sandbox.js`\n * is byte-identical from 0.7.0 through 0.9.0 (npm latest) and still unfixed on\n * `paritytech/host-rust-core` main, the repo formerly named truapi. Remove once\n * it clears the cached client on close.\n */\nfunction latchDisconnected(\n previous: HostConnectionStatus | null,\n next: HostConnectionStatus,\n): HostConnectionStatus {\n return next === \"connecting\" && previous === \"disconnected\" ? \"disconnected\" : next;\n}\n\nfunction notifyLocalStatusListeners(status: HostConnectionStatus): void {\n // Iterate a snapshot: a listener that unsubscribes itself, or re-enters\n // `setTruApiClient`, must not mutate the set mid-loop.\n for (const listener of [...localStatusListeners]) listener(status);\n}\n\n/**\n * Test-only: push `status` to every {@link subscribeConnectionStatus} subscriber,\n * so a product can exercise its reconnecting / offline UI. The host-side\n * counterpart of `@parity/product-sdk-signer`'s `FakeSignerProvider.emitStatus`.\n * Exposed through `@parity/product-sdk-host/testing`, not the main entry.\n */\nexport function emitConnectionStatus(status: HostConnectionStatus): void {\n if (isProductionBuild()) {\n console.warn(\n \"[product-sdk] emitConnectionStatus() was called in a production build. This is a test-only seam from @parity/product-sdk-host/testing; a leaked import will report a fabricated connection status to real subscribers.\",\n );\n }\n notifyLocalStatusListeners(status);\n}\n\n/**\n * Subscribe to host-channel connection status. The callback fires synchronously\n * with the current status and again on every change; the returned function\n * unsubscribes. Repeats of the status you already have are suppressed.\n *\n * This is the **transport** channel. For the host's account-level connection —\n * what drives `@parity/product-sdk-signer`'s `ConnectionStatus` — use\n * `AccountsProvider.subscribeAccountConnectionStatus` instead.\n *\n * Subscribing is not passive: outside an established channel the first subscribe\n * builds the client and provider, so this can be what constructs the transport.\n *\n * Honours the `setTruApiClient` seam — an injected client is connected by\n * definition, and injecting or clearing one notifies live subscribers.\n */\nexport function subscribeConnectionStatus(\n callback: (status: HostConnectionStatus) => void,\n): () => void {\n let last: HostConnectionStatus | null = null;\n\n // One wrapped callback for both sources, so `last` stays coherent: the seam\n // and the sandbox must not each keep their own idea of what was delivered.\n const deliver = (status: HostConnectionStatus, fromSandbox: boolean): void => {\n const next = fromSandbox ? latchDisconnected(last, status) : status;\n if (next === last) return;\n last = next;\n callback(next);\n };\n\n const onLocal = (status: HostConnectionStatus) => deliver(status, false);\n localStatusListeners.add(onLocal);\n\n if (clientOverride !== null) {\n onLocal(\"connected\");\n return () => void localStatusListeners.delete(onLocal);\n }\n\n const unsubscribeSandbox = sandboxSubscribeConnectionStatus((status) => deliver(status, true));\n return () => {\n localStatusListeners.delete(onLocal);\n unsubscribeSandbox();\n };\n}\n\n/**\n * Adapt a truapi `ObservableLike` stream into the host's callback-style\n * {@link HostSubscription} (`unsubscribe` + `onInterrupt`). `onNext` fires for\n * each item; the registered `onInterrupt` callback fires when the host ends the\n * subscription server-side — which the generated client surfaces as either\n * `complete` (a host interrupt frame) or `error` (transport close). Shared by\n * the statement-store and preimage adapters, which both expose this shape.\n */\nexport function subscribeWithInterrupt<Item, Reason = never>(\n observable: ObservableLike<Item, Reason>,\n onNext: (item: Item) => void,\n): TransportSubscription {\n let interruptCallback: ((reason?: unknown) => void) | undefined;\n const sub = observable.subscribe({\n next: onNext,\n error: (reason) => interruptCallback?.(reason),\n complete: () => interruptCallback?.(),\n });\n return {\n subscriptionId: sub.subscriptionId,\n unsubscribe: () => sub.unsubscribe(),\n onInterrupt: (callback) => {\n interruptCallback = callback;\n return () => {\n if (interruptCallback === callback) interruptCallback = undefined;\n };\n },\n };\n}\n\nif (import.meta.vitest) {\n const { test, expect, afterEach } = import.meta.vitest;\n\n afterEach(() => setTruApiClient(null));\n\n // Environment detection and client building are covered by `@parity/truapi`'s\n // own sandbox tests; here we only assert the local glue degrades outside a\n // host container.\n test(\"getClientSync returns null outside a container\", () => {\n expect(getClientSync()).toBeNull();\n });\n\n test(\"getClient resolves null outside a container\", async () => {\n expect(await getClient()).toBeNull();\n });\n\n test(\"setTruApiClient overrides the client and container detection\", async () => {\n const fake = {} as TrUApiClient;\n setTruApiClient(fake);\n expect(getClientSync()).toBe(fake);\n expect(await getClient()).toBe(fake);\n expect(isCorrectEnvironment()).toBe(true);\n\n setTruApiClient(null);\n expect(getClientSync()).toBeNull();\n expect(isCorrectEnvironment()).toBe(false);\n });\n\n test(\"setTruApiClient warns when injecting in a production build\", () => {\n const original = process.env.NODE_ENV;\n const warnings: string[] = [];\n const realWarn = console.warn;\n console.warn = (...args: unknown[]) => void warnings.push(String(args[0]));\n try {\n process.env.NODE_ENV = \"production\";\n setTruApiClient({} as TrUApiClient);\n expect(warnings).toHaveLength(1);\n expect(warnings[0]).toContain(\"production build\");\n\n // Clearing the override must not warn.\n setTruApiClient(null);\n expect(warnings).toHaveLength(1);\n } finally {\n console.warn = realWarn;\n process.env.NODE_ENV = original;\n }\n });\n\n test(\"subscribeConnectionStatus reports disconnected outside a container\", () => {\n const statuses: HostConnectionStatus[] = [];\n\n const unsubscribe = subscribeConnectionStatus((status) => statuses.push(status));\n\n expect(statuses).toEqual([\"disconnected\"]);\n unsubscribe();\n });\n\n test(\"subscribeConnectionStatus reports connected for an injected client\", () => {\n setTruApiClient({} as TrUApiClient);\n const statuses: HostConnectionStatus[] = [];\n\n const unsubscribe = subscribeConnectionStatus((status) => statuses.push(status));\n\n // The sandbox only tracks the client it built itself, so without the\n // override branch this would report \"disconnected\" while every other\n // accessor resolved the injected client.\n expect(statuses).toEqual([\"connected\"]);\n unsubscribe();\n });\n\n test(\"disposing the injected client notifies live subscribers\", () => {\n setTruApiClient({} as TrUApiClient);\n const statuses: HostConnectionStatus[] = [];\n const unsubscribe = subscribeConnectionStatus((status) => statuses.push(status));\n\n setTruApiClient(null);\n\n expect(statuses).toEqual([\"connected\", \"disconnected\"]);\n unsubscribe();\n });\n\n test(\"injecting a client notifies subscribers that started without one\", () => {\n const statuses: HostConnectionStatus[] = [];\n const unsubscribe = subscribeConnectionStatus((status) => statuses.push(status));\n\n setTruApiClient({} as TrUApiClient);\n\n expect(statuses).toEqual([\"disconnected\", \"connected\"]);\n unsubscribe();\n });\n\n test(\"unsubscribe stops seam notifications\", () => {\n setTruApiClient({} as TrUApiClient);\n const statuses: HostConnectionStatus[] = [];\n const unsubscribe = subscribeConnectionStatus((status) => statuses.push(status));\n\n unsubscribe();\n setTruApiClient(null);\n\n expect(statuses).toEqual([\"connected\"]);\n expect(localStatusListeners.size).toBe(0);\n });\n\n test(\"emitConnectionStatus drives transitions, including a reconnect\", () => {\n setTruApiClient({} as TrUApiClient);\n const statuses: HostConnectionStatus[] = [];\n const unsubscribe = subscribeConnectionStatus((status) => statuses.push(status));\n\n emitConnectionStatus(\"disconnected\");\n // A seam-pushed \"connecting\" after \"disconnected\" is deliberate, so it must\n // survive the sandbox latch — otherwise no test could drive a reconnect.\n emitConnectionStatus(\"connecting\");\n emitConnectionStatus(\"connected\");\n\n expect(statuses).toEqual([\"connected\", \"disconnected\", \"connecting\", \"connected\"]);\n unsubscribe();\n });\n\n test(\"repeats of the current status are suppressed\", () => {\n setTruApiClient({} as TrUApiClient);\n const statuses: HostConnectionStatus[] = [];\n const unsubscribe = subscribeConnectionStatus((status) => statuses.push(status));\n\n emitConnectionStatus(\"connected\");\n emitConnectionStatus(\"connected\");\n\n expect(statuses).toEqual([\"connected\"]);\n unsubscribe();\n });\n\n test(\"a listener that unsubscribes itself mid-notification is safe\", () => {\n setTruApiClient({} as TrUApiClient);\n const statuses: HostConnectionStatus[] = [];\n const handle: { unsubscribe?: () => void } = {};\n handle.unsubscribe = subscribeConnectionStatus((status) => {\n statuses.push(status);\n handle.unsubscribe?.();\n });\n\n setTruApiClient(null);\n\n expect(statuses).toEqual([\"connected\", \"disconnected\"]);\n expect(localStatusListeners.size).toBe(0);\n });\n\n // The sandbox latch can't be driven through the public surface — it needs a\n // real provider close — so the correction is pinned as a pure function.\n test(\"latchDisconnected holds disconnected through the stale-cache connecting\", () => {\n expect(latchDisconnected(\"disconnected\", \"connecting\")).toBe(\"disconnected\");\n });\n\n test(\"latchDisconnected passes every other transition through\", () => {\n expect(latchDisconnected(null, \"disconnected\")).toBe(\"disconnected\");\n expect(latchDisconnected(null, \"connecting\")).toBe(\"connecting\");\n expect(latchDisconnected(\"connecting\", \"connected\")).toBe(\"connected\");\n expect(latchDisconnected(\"connected\", \"disconnected\")).toBe(\"disconnected\");\n // A genuine reconnect still gets through once the channel re-establishes.\n expect(latchDisconnected(\"connected\", \"connecting\")).toBe(\"connecting\");\n });\n\n test(\"subscribeWithInterrupt preserves the transport subscription id\", () => {\n const observable = {\n subscribe: () => ({\n subscriptionId: \"p:17\",\n unsubscribe: () => {},\n }),\n [Symbol.observable]() {\n return this;\n },\n } as ObservableLike<never>;\n\n const subscription = subscribeWithInterrupt(observable, () => {});\n\n expect(subscription.subscriptionId).toBe(\"p:17\");\n });\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -5,8 +5,9 @@ import { SdkError } from '@parity/product-sdk-errors';
5
5
  export { SdkError, isSdkError } from '@parity/product-sdk-errors';
6
6
  import { Result } from '@parity/result';
7
7
  export { Result, err, ok } from '@parity/result';
8
- export { H as HostChainDiscovery, a as HostChainIdentifier, g as getHostChainInfo, i as isInsideContainerSync } from './chain-discovery-nLrPzb3d.js';
8
+ export { H as HostChainDiscovery, a as HostChainIdentifier, b as HostConnectionStatus, g as getHostChainInfo, i as isInsideContainerSync, s as subscribeConnectionStatus } from './chain-discovery-BEvu7HaV.js';
9
9
  import { ResultAsync as ResultAsync$1 } from 'neverthrow';
10
+ import '@parity/truapi/sandbox';
10
11
 
11
12
  /**
12
13
  * Public types for the host wrappers.
@@ -156,12 +157,14 @@ declare function getStatementStore(): Promise<HostStatementStore | null>;
156
157
  * chain-specific endpoints used by multiple packages.
157
158
  */
158
159
  /**
159
- * Bulletin Chain RPC endpoints per network environment. `paseo` (Paseo Next v2)
160
- * and `devnet` (public Paseo testnet) are populated today; `polkadot` and
161
- * `kusama` are reserved for when those Bulletin deployments go live.
160
+ * Bulletin Chain RPC endpoints per network environment. `paseo` (Paseo Next v2),
161
+ * `previewnet` (zombienet, a step ahead of paseo), and `devnet` (public Paseo
162
+ * testnet) are populated today; `polkadot` and `kusama` are reserved for when
163
+ * those Bulletin deployments go live.
162
164
  */
163
165
  declare const BULLETIN_RPCS: {
164
166
  readonly paseo: readonly ["wss://paseo-bulletin-next-rpc.polkadot.io"];
167
+ readonly previewnet: readonly ["wss://previewnet.substrate.dev/bulletin"];
165
168
  readonly devnet: readonly ["wss://bulletin-paseo.tservices.es:8443"];
166
169
  readonly polkadot: string[];
167
170
  readonly kusama: string[];
@@ -247,6 +250,29 @@ declare class HostCallFailedError extends HostError {
247
250
  readonly payload: HostErrorPayload;
248
251
  constructor(label: string, payload: HostErrorPayload);
249
252
  }
253
+ /**
254
+ * A host call could not be processed to completion: the `ResultAsync` the
255
+ * truapi client returns rejected instead of resolving to an ok/err. The usual
256
+ * cause is a response the client's SCALE codec can't decode (a
257
+ * `RangeError: Offset is outside the bounds of the DataView`) because the host
258
+ * and the `@parity/truapi` version the product is built against disagree on the
259
+ * wire shape of that call — a protocol-version skew. A host channel that closed
260
+ * mid-call looks identical from here, so this does not assert the skew; the
261
+ * real error is preserved on {@link cause}.
262
+ *
263
+ * The truapi client catches the decode throw in its message handler and turns
264
+ * it into a promise rejection, then wraps the call with
265
+ * `ResultAsync.fromSafePromise`, which installs no rejection handler — so the
266
+ * rejection escapes the `Result` channel rather than landing on its err side.
267
+ * Without this boundary that surfaces as a raw `RangeError` with a stack naming
268
+ * neither the call nor the cause. This names the call, so a bug report has
269
+ * somewhere to start.
270
+ */
271
+ declare class HostResponseDecodeError extends HostError {
272
+ /** The host-API call whose response failed to decode, e.g. `"createRingVRFProof"`. */
273
+ readonly call: string;
274
+ constructor(call: string, cause: unknown);
275
+ }
250
276
  /** Check whether a value is any {@link HostError}. */
251
277
  declare function isHostError(error: unknown): error is HostError;
252
278
 
@@ -492,6 +518,13 @@ type VrfTranscriptItem = {
492
518
  type VrfSignature = {
493
519
  [K in keyof VrfSignature$1]: Uint8Array;
494
520
  };
521
+ /**
522
+ * A call's declared `Err` channel, plus {@link HostResponseDecodeError}: any
523
+ * host reply can fail to decode if the host and the product's `@parity/truapi`
524
+ * client are on different protocol versions, so every decoded call can surface
525
+ * it in addition to its own typed errors.
526
+ */
527
+ type WithDecodeError<E> = E | HostResponseDecodeError;
495
528
  /**
496
529
  * Accounts provider handle, backed by `truApi.account.*` / `truApi.signing.*`.
497
530
  * Surfaces the user's wallet accounts, app-scoped product accounts, Ring VRF,
@@ -500,14 +533,17 @@ type VrfSignature = {
500
533
  * Lookup methods return a neverthrow `ResultAsync` (use `.match(ok, err)`);
501
534
  * the signer factories return a synchronous PAPI `PolkadotSigner`. The `err`
502
535
  * channel carries truapi's canonical `CallErrorValue` envelope around the
503
- * per-call versioned domain error, exactly as the generated client returns it.
536
+ * per-call versioned domain error, exactly as the generated client returns it,
537
+ * plus a {@link HostResponseDecodeError} for the case where the host's reply
538
+ * cannot be decoded at all (a host/client protocol-version skew) — see
539
+ * {@link WithDecodeError}.
504
540
  */
505
541
  interface AccountsProvider {
506
542
  getUserId(): ResultAsync$1<{
507
543
  primaryUsername: string;
508
- }, scale.CallErrorValue<VersionedHostGetUserIdError>>;
509
- requestLogin(reason?: string): ResultAsync$1<HostRequestLoginResponse, scale.CallErrorValue<VersionedHostRequestLoginError>>;
510
- getProductAccount(dotNsIdentifier: string, derivationIndex?: number): ResultAsync$1<ProductAccount, scale.CallErrorValue<VersionedHostAccountGetError>>;
544
+ }, WithDecodeError<scale.CallErrorValue<VersionedHostGetUserIdError>>>;
545
+ requestLogin(reason?: string): ResultAsync$1<HostRequestLoginResponse, WithDecodeError<scale.CallErrorValue<VersionedHostRequestLoginError>>>;
546
+ getProductAccount(dotNsIdentifier: string, derivationIndex?: number): ResultAsync$1<ProductAccount, WithDecodeError<scale.CallErrorValue<VersionedHostAccountGetError>>>;
511
547
  /**
512
548
  * Register a ring-VRF key owned by the calling product.
513
549
  *
@@ -517,17 +553,17 @@ interface AccountsProvider {
517
553
  * Registration returns the key's public key. Call {@link listRingVrfKeys}
518
554
  * afterward to obtain the opaque handle required by alias and proof calls.
519
555
  */
520
- registerRingVrfKey(index: number, ring: RingLocation): ResultAsync$1<RingVrfPublicKey, scale.CallErrorValue<VersionedHostAccountRegisterRingVrfKeyError>>;
556
+ registerRingVrfKey(index: number, ring: RingLocation): ResultAsync$1<RingVrfPublicKey, WithDecodeError<scale.CallErrorValue<VersionedHostAccountRegisterRingVrfKeyError>>>;
521
557
  /** List an owner's registered ring-VRF keys. */
522
- listRingVrfKeys(owner: string, disclosure?: RingVrfKeyDisclosure): ResultAsync$1<RegisteredRingVrfKey[], scale.CallErrorValue<VersionedHostAccountListRingVrfKeysError>>;
558
+ listRingVrfKeys(owner: string, disclosure?: RingVrfKeyDisclosure): ResultAsync$1<RegisteredRingVrfKey[], WithDecodeError<scale.CallErrorValue<VersionedHostAccountListRingVrfKeysError>>>;
523
559
  /** Derive a contextual alias with an explicitly registered ring-VRF key. */
524
- getProductAccountAlias(keyHandle: RingVrfKeyHandle, context: ProductProofContext, location: RingLocation): ResultAsync$1<ContextualAlias, scale.CallErrorValue<VersionedHostAccountGetAliasError>>;
525
- getLegacyAccounts(): ResultAsync$1<HostAccount[], scale.CallErrorValue<VersionedHostGetLegacyAccountsError>>;
560
+ getProductAccountAlias(keyHandle: RingVrfKeyHandle, context: ProductProofContext, location: RingLocation): ResultAsync$1<ContextualAlias, WithDecodeError<scale.CallErrorValue<VersionedHostAccountGetAliasError>>>;
561
+ getLegacyAccounts(): ResultAsync$1<HostAccount[], WithDecodeError<scale.CallErrorValue<VersionedHostGetLegacyAccountsError>>>;
526
562
  /**
527
563
  * Generate a Ring VRF proof with an explicitly registered key, binding
528
564
  * `message` to the product-scoped `context`.
529
565
  */
530
- createRingVRFProof(keyHandle: RingVrfKeyHandle, context: ProductProofContext, location: RingLocation, message: Uint8Array): ResultAsync$1<RingVRFProof, scale.CallErrorValue<VersionedHostAccountCreateProofError>>;
566
+ createRingVRFProof(keyHandle: RingVrfKeyHandle, context: ProductProofContext, location: RingLocation, message: Uint8Array): ResultAsync$1<RingVRFProof, WithDecodeError<scale.CallErrorValue<VersionedHostAccountCreateProofError>>>;
531
567
  /**
532
568
  * Sign `message` directly with an explicitly registered ring-VRF key.
533
569
  *
@@ -535,7 +571,7 @@ interface AccountsProvider {
535
571
  * membership; it is the plain signature under the member key, for
536
572
  * protocols that carry their own proof.
537
573
  */
538
- ringVrfSign(keyHandle: RingVrfKeyHandle, message: Uint8Array): ResultAsync$1<Uint8Array, scale.CallErrorValue<VersionedHostAccountRingVrfSignError>>;
574
+ ringVrfSign(keyHandle: RingVrfKeyHandle, message: Uint8Array): ResultAsync$1<Uint8Array, WithDecodeError<scale.CallErrorValue<VersionedHostAccountRingVrfSignError>>>;
539
575
  /**
540
576
  * Produce an sr25519 VRF signature from a product account (RFC-0023).
541
577
  *
@@ -555,7 +591,7 @@ interface AccountsProvider {
555
591
  *
556
592
  * Hosts predating the call reject it through the error channel.
557
593
  */
558
- signVrf(account: ProductAccountLookup, transcriptLabel: Uint8Array, items: VrfTranscriptItem[]): ResultAsync$1<VrfSignature, scale.CallErrorValue<VersionedHostAccountSignVrfError>>;
594
+ signVrf(account: ProductAccountLookup, transcriptLabel: Uint8Array, items: VrfTranscriptItem[]): ResultAsync$1<VrfSignature, WithDecodeError<scale.CallErrorValue<VersionedHostAccountSignVrfError>>>;
559
595
  /**
560
596
  * Build a `PolkadotSigner` for a product account. Signing routes through the
561
597
  * host's `createTransaction` path: the host decodes the metadata and forwards
@@ -1107,4 +1143,4 @@ declare function broadcastTransaction(genesisHash: HexString, transaction: HexSt
1107
1143
  */
1108
1144
  declare function stopTransaction(genesisHash: HexString, operationId: string): Promise<Result<void, HostError>>;
1109
1145
 
1110
- export { type AccountsProvider, BULLETIN_RPCS, ChainNotSupportedError, type ChainProperties, type ChainSpec, type ChatBotRegistrationResult, type ChatManager, type ChatReceivedAction, type ChatRoomRegistrationResult, type ContextualAlias, DEFAULT_BULLETIN_ENDPOINT, type DevicePermissionKind, type Feature, type HostAccount, HostCallFailedError, HostError, type HostErrorPayload, type HostLocalStorage, type HostStatementStore, type HostSubscription, HostUnavailableError, type NotificationManager, type PaymentManager, type PreimageManager, type ProductAccount, type ProductAccountLookup, type PushNotificationInput, type RegisteredRingVrfKey, type RemotePermissionItem, type ResultAsync, type RingVRFProof, type RingVrfKeyHandle, type RingVrfPublicKey, type StatementTopicFilter, type StatementsPage, type ThemeMode, type ThemeProvider, type TruApi, type VrfSignature, type VrfTranscriptItem, broadcastTransaction, createHostLocalStorage, createHostPreimageManager, createProofAuthorized, deriveEntropy, featureSupported, findRingVrfKeyHandle, formatHostError, fromHex, getAccountsProvider, getChainSpec, getChatManager, getHostLocalStorage, getHostProvider, getNotificationManager, getPaymentManager, getPreimageManager, getStatementStore, getThemeProvider, getTruApi, isChainSupported, isHostError, isInsideContainer, navigateTo, requestDevicePermission, requestPermission, requestResourceAllocation, stopTransaction, toHex };
1146
+ export { type AccountsProvider, BULLETIN_RPCS, ChainNotSupportedError, type ChainProperties, type ChainSpec, type ChatBotRegistrationResult, type ChatManager, type ChatReceivedAction, type ChatRoomRegistrationResult, type ContextualAlias, DEFAULT_BULLETIN_ENDPOINT, type DevicePermissionKind, type Feature, type HostAccount, HostCallFailedError, HostError, type HostErrorPayload, type HostLocalStorage, HostResponseDecodeError, type HostStatementStore, type HostSubscription, HostUnavailableError, type NotificationManager, type PaymentManager, type PreimageManager, type ProductAccount, type ProductAccountLookup, type PushNotificationInput, type RegisteredRingVrfKey, type RemotePermissionItem, type ResultAsync, type RingVRFProof, type RingVrfKeyHandle, type RingVrfPublicKey, type StatementTopicFilter, type StatementsPage, type ThemeMode, type ThemeProvider, type TruApi, type VrfSignature, type VrfTranscriptItem, type WithDecodeError, broadcastTransaction, createHostLocalStorage, createHostPreimageManager, createProofAuthorized, deriveEntropy, featureSupported, findRingVrfKeyHandle, formatHostError, fromHex, getAccountsProvider, getChainSpec, getChatManager, getHostLocalStorage, getHostProvider, getNotificationManager, getPaymentManager, getPreimageManager, getStatementStore, getThemeProvider, getTruApi, isChainSupported, isHostError, isInsideContainer, navigateTo, requestDevicePermission, requestPermission, requestResourceAllocation, stopTransaction, toHex };