@parity/product-sdk-host 0.0.0-dev.312.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.
@@ -0,0 +1,172 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Higher-level wrappers for the host's feature-support probe.
5
+ *
6
+ * `truApi.system.featureSupported` returns a neverthrow `ResultAsync`;
7
+ * {@link featureSupported} collapses that to a `Result` carrying the host's
8
+ * boolean answer. {@link isChainSupported} is a convenience over the only
9
+ * feature variant the host exposes today (`Chain`).
10
+ *
11
+ * @module
12
+ */
13
+
14
+ import { createLogger } from "@parity/product-sdk-logger";
15
+
16
+ import { type HostError, HostUnavailableError } from "./errors.js";
17
+ import { type Result, err } from "./result.js";
18
+ import { getTruApi, type HexString, mapHostResult } from "./truapi.js";
19
+
20
+ const log = createLogger("host:features");
21
+
22
+ /**
23
+ * A feature the host can be probed for via {@link featureSupported}.
24
+ *
25
+ * The only variant today is `Chain`, carrying the chain's `0x`-prefixed genesis
26
+ * hash. This is a flattened form of truapi's `HostFeatureSupportedRequest`,
27
+ * which nests the hash as `{ tag: "Chain"; value: { genesisHash } }` — we
28
+ * inline `value` as the `HexString` for ergonomics and re-nest it at the call
29
+ * site. New variants surface here as a widening of the union.
30
+ */
31
+ export type Feature = { tag: "Chain"; value: HexString };
32
+
33
+ /**
34
+ * Probe the host for support of a specific feature.
35
+ *
36
+ * Calls `truApi.system.featureSupported`, unwraps the response, and returns the
37
+ * host's boolean answer.
38
+ *
39
+ * @param feature - The feature to probe for.
40
+ * @returns `ok(true)` if the host supports the feature, `ok(false)` otherwise,
41
+ * or `err(HostUnavailableError | HostCallFailedError)`.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * import { featureSupported } from "@parity/product-sdk-host";
46
+ *
47
+ * const r = await featureSupported({ tag: "Chain", value: genesisHash });
48
+ * if (r.ok && r.value) { ... }
49
+ * ```
50
+ */
51
+ export async function featureSupported(feature: Feature): Promise<Result<boolean, HostError>> {
52
+ const truApi = await getTruApi();
53
+ if (!truApi) {
54
+ return err(new HostUnavailableError("featureSupported: TruAPI unavailable"));
55
+ }
56
+ log.debug("featureSupported", { tag: feature.tag });
57
+
58
+ return mapHostResult(
59
+ truApi.system.featureSupported({ tag: feature.tag, value: { genesisHash: feature.value } }),
60
+ (response) => response.supported,
61
+ "featureSupported failed",
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Convenience probe: is the chain with the given genesis hash supported by the
67
+ * host? Wraps {@link featureSupported} for the `Chain` feature variant.
68
+ *
69
+ * @param genesisHash - The chain's `0x`-prefixed genesis hash.
70
+ * @returns `ok(true)` if the host supports the chain, `ok(false)` otherwise, or
71
+ * `err(HostUnavailableError | HostCallFailedError)`.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * import { isChainSupported } from "@parity/product-sdk-host";
76
+ *
77
+ * const r = await isChainSupported(genesisHash);
78
+ * if (!r.ok || !r.value) {
79
+ * tellUserChainUnavailable();
80
+ * }
81
+ * ```
82
+ */
83
+ export async function isChainSupported(
84
+ genesisHash: HexString,
85
+ ): Promise<Result<boolean, HostError>> {
86
+ return featureSupported({ tag: "Chain", value: genesisHash });
87
+ }
88
+
89
+ if (import.meta.vitest) {
90
+ const { test, expect, describe, vi } = import.meta.vitest;
91
+
92
+ async function withMockedTruApi<T>(
93
+ bridge: { system?: { featureSupported?: (req: unknown) => unknown } } | null,
94
+ fn: (mod: typeof import("./features.js")) => Promise<T>,
95
+ ): Promise<T> {
96
+ vi.resetModules();
97
+ vi.doMock("./truapi.js", async (importOriginal) => {
98
+ const original = await importOriginal<typeof import("./truapi.js")>();
99
+ return {
100
+ ...original,
101
+ getTruApi: async () => bridge,
102
+ };
103
+ });
104
+ try {
105
+ const mod = await import("./features.js");
106
+ return await fn(mod);
107
+ } finally {
108
+ vi.doUnmock("./truapi.js");
109
+ vi.resetModules();
110
+ }
111
+ }
112
+
113
+ const okBridge = (supported: boolean) => ({
114
+ system: {
115
+ featureSupported: vi.fn().mockReturnValue({
116
+ match: async (onOk: (v: unknown) => unknown) => onOk({ supported }),
117
+ }),
118
+ },
119
+ });
120
+
121
+ describe("featureSupported", () => {
122
+ test("returns err(HostUnavailableError) when TruAPI is unavailable", async () => {
123
+ await withMockedTruApi(null, async (mod) => {
124
+ const result = await mod.featureSupported({ tag: "Chain", value: "0x00" });
125
+ expect(result.ok).toBe(false);
126
+ if (!result.ok) {
127
+ expect(result.error.name).toBe("HostUnavailableError");
128
+ }
129
+ });
130
+ });
131
+
132
+ test("returns ok with the boolean outcome", async () => {
133
+ await withMockedTruApi(okBridge(true), async (mod) => {
134
+ expect(await mod.featureSupported({ tag: "Chain", value: "0x00" })).toEqual({
135
+ ok: true,
136
+ value: true,
137
+ });
138
+ });
139
+ });
140
+
141
+ test("wraps host errors in err(HostCallFailedError) with a diagnostic message", async () => {
142
+ await withMockedTruApi(
143
+ {
144
+ system: {
145
+ featureSupported: vi.fn().mockReturnValue({
146
+ match: async (
147
+ _onOk: (v: unknown) => unknown,
148
+ onErr: (e: unknown) => unknown,
149
+ ) => onErr({ reason: "boom" }),
150
+ }),
151
+ },
152
+ },
153
+ async (mod) => {
154
+ const result = await mod.featureSupported({ tag: "Chain", value: "0x00" });
155
+ expect(result.ok).toBe(false);
156
+ if (!result.ok) {
157
+ expect(result.error.name).toBe("HostCallFailedError");
158
+ expect(result.error.message).toMatch(/featureSupported failed: boom/);
159
+ }
160
+ },
161
+ );
162
+ });
163
+ });
164
+
165
+ describe("isChainSupported", () => {
166
+ test("delegates to featureSupported with the Chain variant", async () => {
167
+ await withMockedTruApi(okBridge(false), async (mod) => {
168
+ expect(await mod.isChainSupported("0x1234")).toEqual({ ok: true, value: false });
169
+ });
170
+ });
171
+ });
172
+ }
package/src/index.ts ADDED
@@ -0,0 +1,149 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * @parity/product-sdk-host — Detect and talk to the Polkadot Desktop/Mobile host container.
5
+ *
6
+ * Use `isInsideContainer` to branch behavior when running embedded vs. standalone,
7
+ * and `getHostLocalStorage`, `getHostProvider`, and `getStatementStore` to reach
8
+ * the storage, signer, and statement-store APIs the host injects.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+ export {
13
+ isInsideContainer,
14
+ isInsideContainerSync,
15
+ getHostLocalStorage,
16
+ createHostLocalStorage,
17
+ getHostProvider,
18
+ getStatementStore,
19
+ ChainNotSupportedError,
20
+ } from "./container.js";
21
+ export type {
22
+ HostLocalStorage,
23
+ HostStatementStore,
24
+ HostSubscription,
25
+ ProductAccountId,
26
+ SignedStatement,
27
+ Statement,
28
+ StatementProof,
29
+ StatementTopicFilter,
30
+ StatementsPage,
31
+ Topic,
32
+ } from "./types.js";
33
+ export { BULLETIN_RPCS, DEFAULT_BULLETIN_ENDPOINT } from "./chains.js";
34
+
35
+ // TruAPI - @parity/truapi client accessor + convenience wrappers.
36
+ export {
37
+ getTruApi,
38
+ getPreimageManager,
39
+ createHostPreimageManager,
40
+ requestResourceAllocation,
41
+ createProofAuthorized,
42
+ // Hex helpers
43
+ toHex,
44
+ fromHex,
45
+ } from "./truapi.js";
46
+ export type {
47
+ TruApi,
48
+ HexString,
49
+ PreimageManager,
50
+ ResultAsync,
51
+ AllocatableResource,
52
+ AllocationOutcome,
53
+ RemotePermission,
54
+ } from "./truapi.js";
55
+
56
+ // Worker — this product's background worker, called from its rendered surface.
57
+ export { getWorkerManager, WorkerCallError } from "./worker.js";
58
+ export type { WorkerManager, WorkerErrorTag } from "./worker.js";
59
+
60
+ // Host chain discovery.
61
+ export { getHostChainInfo } from "./chain-discovery.js";
62
+ export type { HostChainDiscovery, HostChainIdentifier } from "./chain-discovery.js";
63
+
64
+ // Result type + typed host errors (the throw→Result boundary)
65
+ export { ok, err } from "./result.js";
66
+ export type { Result } from "./result.js";
67
+ export { type SdkError, isSdkError } from "@parity/product-sdk-errors";
68
+ export {
69
+ HostError,
70
+ HostUnavailableError,
71
+ HostCallFailedError,
72
+ isHostError,
73
+ formatHostError,
74
+ } from "./errors.js";
75
+ export type { HostErrorPayload } from "./errors.js";
76
+
77
+ // Accounts — host wallet accounts, product accounts, Ring VRF, and signers.
78
+ export { getAccountsProvider, findRingVrfKeyHandle } from "./accounts.js";
79
+ export type {
80
+ AccountsProvider,
81
+ DerivationIndex,
82
+ HostAccount,
83
+ ProductAccount,
84
+ ProductAccountLookup,
85
+ ContextualAlias,
86
+ ProductProofContext,
87
+ RegisteredRingVrfKey,
88
+ RingLocation,
89
+ RingVRFProof,
90
+ RingVrfKeyDisclosure,
91
+ RingVrfKeyHandle,
92
+ RingVrfPublicKey,
93
+ VrfSignature,
94
+ VrfTranscriptItem,
95
+ } from "./accounts.js";
96
+
97
+ // Higher-level permission wrappers
98
+ export { requestPermission, requestDevicePermission } from "./permissions.js";
99
+ export type { DevicePermissionKind, RemotePermissionItem } from "./permissions.js";
100
+
101
+ // Theme provider
102
+ export { getThemeProvider } from "./theme.js";
103
+ export type { ThemeMode, ThemeName, ThemeProvider, ThemeVariant } from "./theme.js";
104
+
105
+ // Entropy derivation (RFC-0007)
106
+ export { deriveEntropy } from "./entropy.js";
107
+
108
+ // Chat
109
+ export { getChatManager } from "./chat.js";
110
+ export type {
111
+ ChatManager,
112
+ ChatMessageContent,
113
+ ChatReceivedAction,
114
+ ChatRoom,
115
+ ChatRoomRegistrationResult,
116
+ ChatBotRegistrationResult,
117
+ } from "./chat.js";
118
+
119
+ // Payments (RFC-0006)
120
+ export { getPaymentManager } from "./payments.js";
121
+ export type { PaymentManager } from "./payments.js";
122
+ export type {
123
+ HostPaymentBalanceSubscribeItem,
124
+ HostPaymentStatusSubscribeItem,
125
+ PaymentTopUpSource,
126
+ } from "@parity/truapi";
127
+
128
+ // Notifications
129
+ export { getNotificationManager } from "./notifications.js";
130
+ export type {
131
+ NotificationManager,
132
+ NotificationId,
133
+ PushNotificationInput,
134
+ PushNotificationError,
135
+ } from "./notifications.js";
136
+
137
+ // Deep-link navigation
138
+ export { navigateTo } from "./navigation.js";
139
+
140
+ // Feature / chain support probes
141
+ export { featureSupported, isChainSupported } from "./features.js";
142
+ export type { Feature } from "./features.js";
143
+
144
+ // Chain spec lookups
145
+ export { getChainSpec } from "./chain-spec.js";
146
+ export type { ChainSpec, ChainProperties } from "./chain-spec.js";
147
+
148
+ // Transaction broadcast lifecycle
149
+ export { broadcastTransaction, stopTransaction } from "./chain-transaction.js";
@@ -0,0 +1,128 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Higher-level wrapper for the host's deep-link navigation.
5
+ *
6
+ * `truApi.system.navigateTo` returns a neverthrow `ResultAsync`; consumers
7
+ * still have to unwrap it themselves. {@link navigateTo} collapses that to a
8
+ * `Result<void, HostError>`-returning Promise.
9
+ *
10
+ * @module
11
+ */
12
+
13
+ import { createLogger } from "@parity/product-sdk-logger";
14
+
15
+ import { type HostError, HostUnavailableError } from "./errors.js";
16
+ import { type Result, err } from "./result.js";
17
+ import { getTruApi, mapHostResult } from "./truapi.js";
18
+
19
+ const log = createLogger("host:navigation");
20
+
21
+ /**
22
+ * Ask the host to navigate to a URL (deep link or external link).
23
+ *
24
+ * Calls `truApi.system.navigateTo` and unwraps the response. The host resolves
25
+ * the destination itself — a `dot`-suffixed deep link (e.g.
26
+ * `"https://search.dot"`) routes to another app/route inside the container, an
27
+ * `https://` URL opens externally.
28
+ *
29
+ * @param url - The URL to navigate to.
30
+ * @returns `ok` on success, or `err`: {@link HostUnavailableError} if the host
31
+ * is unavailable, or {@link HostCallFailedError} if it denies the navigation
32
+ * (`NavigateToErr::PermissionDenied`) or fails otherwise (`NavigateToErr::Unknown`).
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import { navigateTo } from "@parity/product-sdk-host";
37
+ *
38
+ * const r = await navigateTo("https://search.dot");
39
+ * if (!r.ok) handle(r.error);
40
+ * ```
41
+ */
42
+ export async function navigateTo(url: string): Promise<Result<void, HostError>> {
43
+ const truApi = await getTruApi();
44
+ if (!truApi) {
45
+ return err(new HostUnavailableError("navigateTo: TruAPI unavailable"));
46
+ }
47
+ log.debug("navigateTo", { url });
48
+
49
+ return mapHostResult(truApi.system.navigateTo({ url }), () => undefined, "navigateTo failed");
50
+ }
51
+
52
+ if (import.meta.vitest) {
53
+ const { test, expect, describe, vi } = import.meta.vitest;
54
+
55
+ async function withMockedTruApi<T>(
56
+ bridge: { system?: { navigateTo?: (req: unknown) => unknown } } | null,
57
+ fn: (mod: typeof import("./navigation.js")) => Promise<T>,
58
+ ): Promise<T> {
59
+ vi.resetModules();
60
+ vi.doMock("./truapi.js", async (importOriginal) => {
61
+ const original = await importOriginal<typeof import("./truapi.js")>();
62
+ return {
63
+ ...original,
64
+ getTruApi: async () => bridge,
65
+ };
66
+ });
67
+ try {
68
+ const mod = await import("./navigation.js");
69
+ return await fn(mod);
70
+ } finally {
71
+ vi.doUnmock("./truapi.js");
72
+ vi.resetModules();
73
+ }
74
+ }
75
+
76
+ describe("navigateTo", () => {
77
+ test("returns err(HostUnavailableError) when TruAPI is unavailable", async () => {
78
+ await withMockedTruApi(null, async (mod) => {
79
+ const result = await mod.navigateTo("https://search.dot");
80
+ expect(result.ok).toBe(false);
81
+ if (!result.ok) {
82
+ expect(result.error.name).toBe("HostUnavailableError");
83
+ }
84
+ });
85
+ });
86
+
87
+ test("returns ok on success", async () => {
88
+ await withMockedTruApi(
89
+ {
90
+ system: {
91
+ navigateTo: vi.fn().mockReturnValue({
92
+ match: async (onOk: (v: unknown) => unknown) => onOk(undefined),
93
+ }),
94
+ },
95
+ },
96
+ async (mod) => {
97
+ expect(await mod.navigateTo("https://search.dot")).toEqual({
98
+ ok: true,
99
+ value: undefined,
100
+ });
101
+ },
102
+ );
103
+ });
104
+
105
+ test("wraps host errors in err(HostCallFailedError) with a diagnostic message", async () => {
106
+ await withMockedTruApi(
107
+ {
108
+ system: {
109
+ navigateTo: vi.fn().mockReturnValue({
110
+ match: async (
111
+ _onOk: (v: unknown) => unknown,
112
+ onErr: (e: unknown) => unknown,
113
+ ) => onErr({ tag: "PermissionDenied" }),
114
+ }),
115
+ },
116
+ },
117
+ async (mod) => {
118
+ const result = await mod.navigateTo("https://search.dot");
119
+ expect(result.ok).toBe(false);
120
+ if (!result.ok) {
121
+ expect(result.error.name).toBe("HostCallFailedError");
122
+ expect(result.error.message).toMatch(/navigateTo failed: PermissionDenied/);
123
+ }
124
+ },
125
+ );
126
+ });
127
+ });
128
+ }
@@ -0,0 +1,113 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Wrapper for the host's scheduled push-notification surface (RFC-0019),
5
+ * backed by `truApi.notifications.*`.
6
+ *
7
+ * `getNotificationManager()` returns a handle exposing `push(input)` (resolves
8
+ * to a {@link NotificationId}) and `cancel(id)`, matching the singleton
9
+ * pattern already used by {@link getPaymentManager}, {@link getPreimageManager},
10
+ * and {@link getHostLocalStorage}.
11
+ *
12
+ * @module
13
+ */
14
+
15
+ import type { HostPushNotificationRequest, NotificationId, TrUApiClient } from "@parity/truapi";
16
+
17
+ import { getClient } from "./transport.js";
18
+ import { unwrapHostResult } from "./truapi.js";
19
+
20
+ /**
21
+ * Error variants the host raises when scheduling a push notification.
22
+ *
23
+ * A `{ tag }` tagged union re-exported from `@parity/truapi`:
24
+ * `{ tag: "ScheduleLimitReached" }` (the host-wide pending-notification cap) or
25
+ * `{ tag: "Unknown"; value: { reason } }`. {@link NotificationManager.push} /
26
+ * {@link NotificationManager.cancel} reject with an `Error` whose `cause`
27
+ * carries this value, so branch on it — e.g.
28
+ * `(err as Error).cause?.tag === "ScheduleLimitReached"`.
29
+ */
30
+ export type { HostPushNotificationError as PushNotificationError } from "@parity/truapi";
31
+
32
+ /**
33
+ * Host-assigned id for a scheduled notification — pass to
34
+ * {@link NotificationManager.cancel}. Re-exported from `@parity/truapi`.
35
+ */
36
+ export type { NotificationId };
37
+
38
+ /**
39
+ * Push payload: `text`, an optional `deeplink`, and an optional `scheduledAt`
40
+ * (Unix timestamp in milliseconds; omit for immediate delivery). Re-exported
41
+ * from the truapi wire request type so the shape stays in lockstep with the
42
+ * protocol.
43
+ */
44
+ export type PushNotificationInput = HostPushNotificationRequest;
45
+
46
+ /**
47
+ * Host notification manager handle. Exposes `push(input)` (resolves to a
48
+ * {@link NotificationId}) and `cancel(id)`.
49
+ */
50
+ export interface NotificationManager {
51
+ push(input: PushNotificationInput): Promise<NotificationId>;
52
+ cancel(id: NotificationId): Promise<void>;
53
+ }
54
+
55
+ /** Build a {@link NotificationManager} over a TruAPI client's `notifications` domain. */
56
+ function adaptNotificationManager(client: TrUApiClient): NotificationManager {
57
+ const notifications = client.notifications;
58
+ return {
59
+ async push(input) {
60
+ const response = await unwrapHostResult(
61
+ notifications.sendPushNotification(input),
62
+ "notification push failed",
63
+ );
64
+ return response.id;
65
+ },
66
+ async cancel(id) {
67
+ await unwrapHostResult(
68
+ notifications.cancelPushNotification({ id }),
69
+ "notification cancel failed",
70
+ );
71
+ },
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Get the host notification manager, backed by `truApi.notifications.*`.
77
+ * Returns `null` when running outside a host container.
78
+ *
79
+ * @returns The notification manager, or `null` if unavailable.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * import { getNotificationManager, type PushNotificationError } from "@parity/product-sdk-host";
84
+ *
85
+ * const notifications = await getNotificationManager();
86
+ * if (notifications) {
87
+ * try {
88
+ * const id = await notifications.push({
89
+ * text: "Doors open in 1h",
90
+ * scheduledAt: someUnixMs,
91
+ * });
92
+ * // later: await notifications.cancel(id);
93
+ * } catch (err) {
94
+ * const cause = (err as Error).cause as PushNotificationError | undefined;
95
+ * if (cause?.tag === "ScheduleLimitReached") {
96
+ * // host hit its pending-notification cap — surface to the user
97
+ * }
98
+ * }
99
+ * }
100
+ * ```
101
+ */
102
+ export async function getNotificationManager(): Promise<NotificationManager | null> {
103
+ const client = await getClient();
104
+ return client ? adaptNotificationManager(client) : null;
105
+ }
106
+
107
+ if (import.meta.vitest) {
108
+ const { test, expect } = import.meta.vitest;
109
+
110
+ test("getNotificationManager returns null outside a container", async () => {
111
+ expect(await getNotificationManager()).toBeNull();
112
+ });
113
+ }