@parity/product-sdk-host 0.3.0 → 0.5.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/chat.ts ADDED
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Wrapper for the host's chat surface (`host_chat_*` family).
3
+ *
4
+ * Shipped flat-in-host rather than as `getTruApi().chat.*` (the shape
5
+ * sketched in issue #93) because the upstream JS `hostApi` is itself a
6
+ * flat object - there is no `.chat` accessor to mirror. A flat
7
+ * `getChatManager()` matches the pattern already used by
8
+ * {@link getThemeProvider}, {@link getAccountsProvider}, and
9
+ * {@link getStatementStore}; if a namespaced view is desirable later, it
10
+ * can be layered on top without breaking this surface.
11
+ *
12
+ * @module
13
+ */
14
+
15
+ import { createLogger } from "@parity/product-sdk-logger";
16
+
17
+ import type {
18
+ ChatBotRegistrationResult as NovasamaChatBotRegistrationResult,
19
+ ChatCustomMessageRenderer as NovasamaChatCustomMessageRenderer,
20
+ ChatCustomMessageRendererParams as NovasamaChatCustomMessageRendererParams,
21
+ ChatMessageContent as NovasamaChatMessageContent,
22
+ ChatReceivedAction as NovasamaChatReceivedAction,
23
+ ChatRoom as NovasamaChatRoom,
24
+ ChatRoomRegistrationResult as NovasamaChatRoomRegistrationResult,
25
+ createProductChatManager,
26
+ } from "@novasamatech/host-api-wrapper";
27
+
28
+ const log = createLogger("host:chat");
29
+
30
+ /** Chat message payload variants. Re-exported from `@novasamatech/host-api-wrapper`. */
31
+ export type ChatMessageContent = NovasamaChatMessageContent;
32
+
33
+ /** Action received via {@link ChatManager.subscribeAction}. Re-exported from `@novasamatech/host-api-wrapper`. */
34
+ export type ChatReceivedAction = NovasamaChatReceivedAction;
35
+
36
+ /** Room metadata delivered to {@link ChatManager.subscribeChatList}. Re-exported from `@novasamatech/host-api-wrapper`. */
37
+ export type ChatRoom = NovasamaChatRoom;
38
+
39
+ /** Result of registering a chat room (`"New" | "Exists"`). Re-exported from `@novasamatech/host-api-wrapper`. */
40
+ export type ChatRoomRegistrationResult = NovasamaChatRoomRegistrationResult;
41
+
42
+ /** Result of registering a bot (`"New" | "Exists"`). Re-exported from `@novasamatech/host-api-wrapper`. */
43
+ export type ChatBotRegistrationResult = NovasamaChatBotRegistrationResult;
44
+
45
+ /** Renderer callback for custom message types. Re-exported from `@novasamatech/host-api-wrapper`. */
46
+ export type ChatCustomMessageRenderer = NovasamaChatCustomMessageRenderer;
47
+
48
+ /** Parameters passed to a {@link ChatCustomMessageRenderer}. Re-exported from `@novasamatech/host-api-wrapper`. */
49
+ export type ChatCustomMessageRendererParams<T = Uint8Array> =
50
+ NovasamaChatCustomMessageRendererParams<T>;
51
+
52
+ /**
53
+ * Chat manager handle. Exposes room/bot registration, message sending,
54
+ * subscription to room list and incoming actions, and custom-renderer
55
+ * registration.
56
+ *
57
+ * Type identical to `createProductChatManager()` from
58
+ * `@novasamatech/host-api-wrapper`.
59
+ */
60
+ export type ChatManager = ReturnType<typeof createProductChatManager>;
61
+
62
+ /**
63
+ * Get the host chat manager.
64
+ *
65
+ * Returns the chat manager from `@novasamatech/host-api-wrapper`, or `null` if
66
+ * the package is unavailable (running outside a host container or the
67
+ * optional peer dep isn't installed).
68
+ *
69
+ * @returns The chat manager, or `null` if unavailable.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * import { getChatManager } from "@parity/product-sdk-host";
74
+ *
75
+ * const chat = await getChatManager();
76
+ * if (chat) {
77
+ * await chat.registerBot({ botId: "echo", name: "Echo Bot", icon: "" });
78
+ * chat.subscribeAction((action) => { ... });
79
+ * }
80
+ * ```
81
+ */
82
+ export async function getChatManager(): Promise<ChatManager | null> {
83
+ try {
84
+ const sdk = await import("@novasamatech/host-api-wrapper");
85
+ return sdk.createProductChatManager();
86
+ } catch (err) {
87
+ log.debug("getChatManager unavailable", err);
88
+ return null;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Dispatch helper that composes multiple custom-message renderers into a
94
+ * single {@link ChatCustomMessageRenderer} keyed by `messageType`.
95
+ *
96
+ * Mirrors `matchChatCustomRenderers` from `@novasamatech/host-api-wrapper`
97
+ * inline (the upstream implementation is pure dispatch logic with no
98
+ * transport / runtime dependency on Novasama), so callers get the same
99
+ * sync signature instead of an async-with-null wrapper.
100
+ *
101
+ * @param map - Object mapping `messageType` strings to renderers.
102
+ * @returns A composed renderer that dispatches to the entry matching
103
+ * `params.messageType`, or throws if no renderer is registered.
104
+ */
105
+ export function matchChatCustomRenderers(
106
+ map: Record<string, ChatCustomMessageRenderer>,
107
+ ): ChatCustomMessageRenderer {
108
+ return (params, render) => {
109
+ const renderer = map[params.messageType];
110
+ if (!renderer) {
111
+ throw new Error(`Renderer for message type ${params.messageType} is not defined`);
112
+ }
113
+ return renderer(params, render);
114
+ };
115
+ }
116
+
117
+ if (import.meta.vitest) {
118
+ const { test, expect } = import.meta.vitest;
119
+
120
+ test("getChatManager returns manager when SDK is available", async () => {
121
+ const chat = await getChatManager();
122
+ expect(chat === null || typeof chat === "object").toBe(true);
123
+ });
124
+ }
package/src/container.ts CHANGED
@@ -1,7 +1,11 @@
1
1
  import type { JsonRpcProvider } from "polkadot-api";
2
+ import { createLogger } from "@parity/product-sdk-logger";
3
+ import type { Transport } from "@novasamatech/host-api";
2
4
 
3
5
  import type { HostLocalStorage, HostStatementStore } from "./types.js";
4
6
 
7
+ const log = createLogger("host:container");
8
+
5
9
  /**
6
10
  * Detect if running inside a Host container (Polkadot Browser / Polkadot Desktop).
7
11
  *
@@ -15,7 +19,7 @@ export async function isInsideContainer(): Promise<boolean> {
15
19
  if (typeof window === "undefined") return false;
16
20
 
17
21
  try {
18
- const sdk = await import("@novasamatech/product-sdk");
22
+ const sdk = await import("@novasamatech/host-api-wrapper");
19
23
  return sdk.sandboxProvider.isCorrectEnvironment();
20
24
  } catch {
21
25
  return isInsideContainerSync();
@@ -30,9 +34,35 @@ export async function getHostLocalStorage(): Promise<HostLocalStorage | null> {
30
34
  if (!(await isInsideContainer())) return null;
31
35
 
32
36
  try {
33
- const sdk = await import("@novasamatech/product-sdk");
37
+ const sdk = await import("@novasamatech/host-api-wrapper");
34
38
  return sdk.hostLocalStorage as HostLocalStorage;
35
- } catch {
39
+ } catch (err) {
40
+ log.debug("getHostLocalStorage unavailable", err);
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Construct a fresh host-backed `HostLocalStorage` instance with an optional
47
+ * custom transport. Use this when you need a non-default transport (e.g.
48
+ * for tests); otherwise prefer {@link getHostLocalStorage}, which returns
49
+ * the shared singleton.
50
+ *
51
+ * Mirrors `createLocalStorage` from `@novasamatech/host-api-wrapper`.
52
+ *
53
+ * @param transport - Optional transport; defaults to the sandbox transport.
54
+ * @returns A new `HostLocalStorage` instance, or `null` if unavailable.
55
+ */
56
+ export async function createHostLocalStorage(
57
+ transport?: Transport,
58
+ ): Promise<HostLocalStorage | null> {
59
+ if (!(await isInsideContainer())) return null;
60
+
61
+ try {
62
+ const sdk = await import("@novasamatech/host-api-wrapper");
63
+ return sdk.createLocalStorage(transport);
64
+ } catch (err) {
65
+ log.debug("createHostLocalStorage unavailable", err);
36
66
  return null;
37
67
  }
38
68
  }
@@ -42,16 +72,17 @@ export async function getHostLocalStorage(): Promise<HostLocalStorage | null> {
42
72
  *
43
73
  * When running inside a Polkadot container, this wraps the chain connection via the
44
74
  * host's `createPapiProvider`, enabling shared connections and efficient routing.
45
- * Returns `null` when `@novasamatech/product-sdk` is unavailable.
75
+ * Returns `null` when `@novasamatech/host-api-wrapper` is unavailable.
46
76
  *
47
77
  * @param genesisHash - Genesis hash of the target chain (`0x`-prefixed hex string).
48
78
  * @returns A host-routed `JsonRpcProvider`, or `null` if unavailable.
49
79
  */
50
80
  export async function getHostProvider(genesisHash: `0x${string}`): Promise<JsonRpcProvider | null> {
51
81
  try {
52
- const sdk = await import("@novasamatech/product-sdk");
82
+ const sdk = await import("@novasamatech/host-api-wrapper");
53
83
  return sdk.createPapiProvider(genesisHash);
54
- } catch {
84
+ } catch (err) {
85
+ log.debug("getHostProvider unavailable", err);
55
86
  return null;
56
87
  }
57
88
  }
@@ -90,15 +121,16 @@ export function isInsideContainerSync(): boolean {
90
121
  *
91
122
  * Returns a statement store with `subscribe`, `createProof`, and `submit` methods
92
123
  * that communicate through the host's native binary protocol — bypassing JSON-RPC
93
- * entirely. Returns `null` when `@novasamatech/product-sdk` is unavailable.
124
+ * entirely. Returns `null` when `@novasamatech/host-api-wrapper` is unavailable.
94
125
  *
95
126
  * @returns The host statement store, or `null` if unavailable.
96
127
  */
97
128
  export async function getStatementStore(): Promise<HostStatementStore | null> {
98
129
  try {
99
- const sdk = await import("@novasamatech/product-sdk");
130
+ const sdk = await import("@novasamatech/host-api-wrapper");
100
131
  return sdk.createStatementStore() as HostStatementStore;
101
- } catch {
132
+ } catch (err) {
133
+ log.debug("getStatementStore unavailable", err);
102
134
  return null;
103
135
  }
104
136
  }
@@ -166,6 +198,10 @@ if (import.meta.vitest) {
166
198
  expect(await getHostLocalStorage()).toBeNull();
167
199
  });
168
200
 
201
+ test("createHostLocalStorage returns null outside container", async () => {
202
+ expect(await createHostLocalStorage()).toBeNull();
203
+ });
204
+
169
205
  test("getHostProvider returns null when product-sdk unavailable", async () => {
170
206
  const result = await getHostProvider("0xabc");
171
207
  expect(result).toBeNull();
package/src/entropy.ts ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Higher-level wrapper for the host's entropy derivation (RFC-0007).
3
+ *
4
+ * `hostApi.deriveEntropy` is reachable via {@link getTruApi}, but consumers
5
+ * have to wrap the value in the versioned envelope (`enumValue("v1", ...)`)
6
+ * and unwrap the neverthrow `ResultAsync` themselves. `deriveEntropy`
7
+ * collapses that to a throw-on-error Promise that matches the shape of
8
+ * {@link requestPermission} and {@link requestResourceAllocation}.
9
+ *
10
+ * @module
11
+ */
12
+
13
+ import { createLogger } from "@parity/product-sdk-logger";
14
+
15
+ import { enumValue, formatHostError, getTruApi } from "./truapi.js";
16
+
17
+ const log = createLogger("host:entropy");
18
+
19
+ /**
20
+ * Derive deterministic entropy from a context key (RFC-0007).
21
+ *
22
+ * The host derives entropy from the user's wallet + the provided context
23
+ * key. Calling with the same key on the same wallet yields the same bytes;
24
+ * different keys (or different wallets) yield uncorrelated entropy.
25
+ *
26
+ * @param key - Context key bytes (typically a SCALE-encoded discriminator).
27
+ * @returns The derived entropy bytes.
28
+ * @throws If the host is unavailable or the host-side derivation fails.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * import { deriveEntropy } from "@parity/product-sdk-host";
33
+ *
34
+ * const seed = await deriveEntropy(new TextEncoder().encode("my-app:seed-v1"));
35
+ * ```
36
+ */
37
+ export async function deriveEntropy(key: Uint8Array): Promise<Uint8Array> {
38
+ const truApi = await getTruApi();
39
+ if (!truApi) {
40
+ throw new Error("deriveEntropy: TruAPI unavailable");
41
+ }
42
+ log.debug("deriveEntropy", { keyLen: key.length });
43
+
44
+ return await truApi.deriveEntropy(enumValue("v1", key)).match(
45
+ (envelope: { tag: "v1"; value: Uint8Array }) => envelope.value,
46
+ (err: unknown) => {
47
+ throw new Error(`deriveEntropy failed: ${formatHostError(err)}`, { cause: err });
48
+ },
49
+ );
50
+ }
51
+
52
+ if (import.meta.vitest) {
53
+ const { test, expect } = import.meta.vitest;
54
+
55
+ test("deriveEntropy throws when TruAPI is unavailable", async () => {
56
+ const api = await getTruApi();
57
+ if (api === null) {
58
+ await expect(deriveEntropy(new Uint8Array([1, 2, 3]))).rejects.toThrow(
59
+ /TruAPI unavailable/,
60
+ );
61
+ } else {
62
+ expect(typeof deriveEntropy).toBe("function");
63
+ }
64
+ });
65
+ }
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ export {
11
11
  isInsideContainer,
12
12
  isInsideContainerSync,
13
13
  getHostLocalStorage,
14
+ createHostLocalStorage,
14
15
  getHostProvider,
15
16
  getStatementStore,
16
17
  } from "./container.js";
@@ -18,18 +19,25 @@ export type {
18
19
  HostLocalStorage,
19
20
  HostStatementStore,
20
21
  HostSubscription,
22
+ ProductAccountId,
23
+ SignedStatement,
24
+ Statement,
21
25
  StatementProof,
22
26
  StatementTopicFilter,
23
27
  StatementsPage,
28
+ Topic,
24
29
  } from "./types.js";
25
30
  export { BULLETIN_RPCS, DEFAULT_BULLETIN_ENDPOINT } from "./chains.js";
26
31
 
27
- // TruAPI - re-exports from @novasamatech/product-sdk and @novasamatech/host-api
32
+ // TruAPI - re-exports from @novasamatech/host-api-wrapper and @novasamatech/host-api
28
33
  export {
29
34
  getTruApi,
30
35
  getPreimageManager,
36
+ createHostPreimageManager,
31
37
  getAccountsProvider,
32
38
  requestResourceAllocation,
39
+ createProofAuthorized,
40
+ formatHostError,
33
41
  // Helpers from @novasamatech/host-api
34
42
  enumValue,
35
43
  isEnumVariant,
@@ -50,5 +58,37 @@ export type {
50
58
  ContextualAlias,
51
59
  ResultAsync,
52
60
  AllocatableResource,
61
+ AllocatableResourceTag,
53
62
  AllocationOutcome,
63
+ AllocationOutcomeTag,
64
+ RemotePermission,
65
+ RemotePermissionTag,
54
66
  } from "./truapi.js";
67
+
68
+ // Higher-level permission wrappers
69
+ export { requestPermission, requestDevicePermission } from "./permissions.js";
70
+ export type { DevicePermissionKind, RemotePermissionItem } from "./permissions.js";
71
+
72
+ // Theme provider
73
+ export { getThemeProvider } from "./theme.js";
74
+ export type { ThemeMode, ThemeProvider } from "./theme.js";
75
+
76
+ // Entropy derivation (RFC-0007)
77
+ export { deriveEntropy } from "./entropy.js";
78
+
79
+ // Chat
80
+ export { getChatManager, matchChatCustomRenderers } from "./chat.js";
81
+ export type {
82
+ ChatManager,
83
+ ChatMessageContent,
84
+ ChatReceivedAction,
85
+ ChatRoom,
86
+ ChatRoomRegistrationResult,
87
+ ChatBotRegistrationResult,
88
+ ChatCustomMessageRenderer,
89
+ ChatCustomMessageRendererParams,
90
+ } from "./chat.js";
91
+
92
+ // Payments (RFC-0006)
93
+ export { getPaymentManager } from "./payments.js";
94
+ export type { PaymentManager, PaymentBalance, PaymentStatus, TopUpSource } from "./payments.js";
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Wrapper for the host's payment manager (RFC-0006).
3
+ *
4
+ * Shipped flat-in-host rather than as `getTruApi().payment.*` because the
5
+ * upstream JS `hostApi` is itself a flat object - there is no `.payment`
6
+ * accessor to mirror. A flat `getPaymentManager()` matches the singleton
7
+ * pattern already used by {@link getPreimageManager},
8
+ * {@link getHostLocalStorage}, and {@link getAccountsProvider}.
9
+ *
10
+ * Returns the shared `paymentManager` singleton from
11
+ * `@novasamatech/host-api-wrapper` (not a fresh `createPaymentManager()`
12
+ * instance) so callers share one wrapper + hostApi closure across the app.
13
+ *
14
+ * Distinct from the CoinPayment / merchant-payments surface tracked under
15
+ * `@parity/product-sdk-merchant-payments` (RFC-0017). RFC-0006 is the
16
+ * user-initiated balance / top-up / payment-request flow; RFC-0017 is the
17
+ * merchant-initiated checkout flow.
18
+ *
19
+ * @module
20
+ */
21
+
22
+ import { createLogger } from "@parity/product-sdk-logger";
23
+
24
+ import type {
25
+ PaymentBalance as NovasamaPaymentBalance,
26
+ PaymentStatus as NovasamaPaymentStatus,
27
+ TopUpSource as NovasamaTopUpSource,
28
+ paymentManager,
29
+ } from "@novasamatech/host-api-wrapper";
30
+
31
+ const log = createLogger("host:payments");
32
+
33
+ /** Available balance for the user's payment account. Re-exported from `@novasamatech/host-api-wrapper`. */
34
+ export type PaymentBalance = NovasamaPaymentBalance;
35
+
36
+ /** Status of an in-flight payment request. Re-exported from `@novasamatech/host-api-wrapper`. */
37
+ export type PaymentStatus = NovasamaPaymentStatus;
38
+
39
+ /** Source for {@link PaymentManager.topUp}. Re-exported from `@novasamatech/host-api-wrapper`. */
40
+ export type TopUpSource = NovasamaTopUpSource;
41
+
42
+ /**
43
+ * Payment manager handle. Exposes balance subscription, top-up, payment
44
+ * requests, and payment status subscription.
45
+ *
46
+ * Type identical to `paymentManager` from `@novasamatech/host-api-wrapper`.
47
+ */
48
+ export type PaymentManager = typeof paymentManager;
49
+
50
+ /**
51
+ * Get the host payment manager.
52
+ *
53
+ * Returns the shared `paymentManager` singleton from
54
+ * `@novasamatech/host-api-wrapper`, or `null` if the package is unavailable
55
+ * (running outside a host container or the optional peer dep isn't
56
+ * installed).
57
+ *
58
+ * @returns The payment manager, or `null` if unavailable.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * import { getPaymentManager } from "@parity/product-sdk-host";
63
+ *
64
+ * const payments = await getPaymentManager();
65
+ * if (payments) {
66
+ * const sub = payments.subscribeBalance((b) => { ... });
67
+ * await payments.topUp(1_000_000n, { type: "productAccount", derivationIndex: 0 });
68
+ * const destination = new Uint8Array(32);
69
+ * const { id } = await payments.requestPayment(500n, destination);
70
+ * sub.unsubscribe();
71
+ * }
72
+ * ```
73
+ */
74
+ export async function getPaymentManager(): Promise<PaymentManager | null> {
75
+ try {
76
+ const sdk = await import("@novasamatech/host-api-wrapper");
77
+ return sdk.paymentManager;
78
+ } catch (err) {
79
+ log.debug("getPaymentManager unavailable", err);
80
+ return null;
81
+ }
82
+ }
83
+
84
+ if (import.meta.vitest) {
85
+ const { test, expect } = import.meta.vitest;
86
+
87
+ test("getPaymentManager returns manager with full RFC-0006 surface when SDK is available", async () => {
88
+ const payments = await getPaymentManager();
89
+ if (payments === null) {
90
+ // Acceptable: SDK couldn't load (e.g. peer dep missing in some envs).
91
+ return;
92
+ }
93
+ expect(typeof payments.subscribeBalance).toBe("function");
94
+ expect(typeof payments.topUp).toBe("function");
95
+ expect(typeof payments.requestPayment).toBe("function");
96
+ expect(typeof payments.subscribePaymentStatus).toBe("function");
97
+ });
98
+ }