@parity/product-sdk-host 0.1.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/truapi.ts ADDED
@@ -0,0 +1,371 @@
1
+ /**
2
+ * TruAPI - the protocol for communicating between apps and the Polkadot host container.
3
+ *
4
+ * This module centralizes access to @novasamatech/product-sdk and @novasamatech/host-api,
5
+ * allowing other @parity/product-sdk-* packages to import from here rather than depending
6
+ * directly on novasama packages.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import { createLogger } from "@parity/product-sdk-logger";
12
+
13
+ const log = createLogger("host");
14
+
15
+ // ─────────────────────────────────────────────────────────────────────────────
16
+ // Helpers from @novasamatech/host-api (re-exported from @novasamatech/scale)
17
+ // ─────────────────────────────────────────────────────────────────────────────
18
+
19
+ export {
20
+ /**
21
+ * Construct an enum variant for TruAPI calls.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * import { enumValue, getTruApi } from "@parity/product-sdk-host";
26
+ *
27
+ * const truApi = await getTruApi();
28
+ * if (truApi) {
29
+ * await truApi.permission([enumValue("ChainSubmit")]);
30
+ * }
31
+ * ```
32
+ */
33
+ enumValue,
34
+ /**
35
+ * Check if a value is a specific enum variant.
36
+ */
37
+ isEnumVariant,
38
+ /**
39
+ * Assert that a value is a specific enum variant, throwing if not.
40
+ */
41
+ assertEnumVariant,
42
+ /**
43
+ * Unwrap a Result, throwing on error.
44
+ */
45
+ unwrapResultOrThrow,
46
+ /**
47
+ * Create an Ok result.
48
+ */
49
+ resultOk,
50
+ /**
51
+ * Create an Err result.
52
+ */
53
+ resultErr,
54
+ /**
55
+ * Convert bytes to hex string.
56
+ */
57
+ toHex,
58
+ /**
59
+ * Convert hex string to bytes.
60
+ */
61
+ fromHex,
62
+ } from "@novasamatech/host-api";
63
+
64
+ export type { HexString } from "@novasamatech/host-api";
65
+
66
+ // ─────────────────────────────────────────────────────────────────────────────
67
+ // TruAPI accessor
68
+ // ─────────────────────────────────────────────────────────────────────────────
69
+
70
+ /**
71
+ * The TruApi type - provides low-level methods for communicating with the host.
72
+ *
73
+ * Methods include:
74
+ * - `navigateTo(url)` — Navigate to a URL within the host
75
+ * - `permission(permissions)` — Request permissions from the host
76
+ * - `localStorageRead/Write/Clear` — Host-backed storage
77
+ * - `sign(payload)` — Request transaction signing
78
+ * - `deriveEntropy(context)` — Derive deterministic entropy
79
+ * - `themeSubscribe()` — Subscribe to host theme changes
80
+ * - And many more...
81
+ */
82
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
+ export type TruApi = any;
84
+
85
+ /** Cached TruApi instance */
86
+ let cachedTruApi: TruApi | null = null;
87
+
88
+ /**
89
+ * Get the TruAPI instance for direct low-level access.
90
+ *
91
+ * Returns the `hostApi` object from `@novasamatech/product-sdk` which provides
92
+ * methods for communicating directly with the host container. Returns `null`
93
+ * when running outside a container or when the SDK is unavailable.
94
+ *
95
+ * For most use cases, prefer the higher-level functions like `getHostLocalStorage()`,
96
+ * `getHostProvider()`, etc. Use this when you need direct access to host methods
97
+ * like `navigateTo()`, `permission()`, or `deriveEntropy()`.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * import { getTruApi, enumValue } from "@parity/product-sdk-host";
102
+ *
103
+ * const truApi = await getTruApi();
104
+ * if (truApi) {
105
+ * // Request permission
106
+ * const result = await truApi.permission([enumValue("ChainSubmit")]);
107
+ *
108
+ * // Navigate to a URL
109
+ * await truApi.navigateTo("polkadot://settings");
110
+ *
111
+ * // Subscribe to theme changes
112
+ * const sub = truApi.themeSubscribe(undefined, (theme) => {
113
+ * console.log("Theme changed:", theme);
114
+ * });
115
+ * }
116
+ * ```
117
+ *
118
+ * @returns The TruAPI instance, or `null` if unavailable.
119
+ */
120
+ export async function getTruApi(): Promise<TruApi | null> {
121
+ if (cachedTruApi) return cachedTruApi;
122
+
123
+ try {
124
+ const sdk = await import("@novasamatech/product-sdk");
125
+ cachedTruApi = sdk.hostApi;
126
+ log.debug("TruAPI loaded");
127
+ return cachedTruApi;
128
+ } catch {
129
+ log.debug("TruAPI unavailable (not in container or SDK not installed)");
130
+ return null;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Get the preimage manager for bulletin chain operations.
136
+ *
137
+ * The preimage manager handles uploading and looking up preimages (arbitrary data)
138
+ * on the bulletin chain through the host's optimized path.
139
+ *
140
+ * @returns The preimage manager, or `null` if unavailable.
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * import { getPreimageManager } from "@parity/product-sdk-host";
145
+ *
146
+ * const manager = await getPreimageManager();
147
+ * if (manager) {
148
+ * // Submit a preimage
149
+ * const key = await manager.submit(new Uint8Array([1, 2, 3]));
150
+ *
151
+ * // Look up a preimage
152
+ * const sub = manager.lookup(key, (data) => {
153
+ * if (data) console.log("Found:", data);
154
+ * });
155
+ * }
156
+ * ```
157
+ */
158
+ export async function getPreimageManager(): Promise<PreimageManager | null> {
159
+ try {
160
+ const sdk = await import("@novasamatech/product-sdk");
161
+ return sdk.preimageManager;
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Preimage manager interface for bulletin chain operations.
169
+ */
170
+ export interface PreimageManager {
171
+ /**
172
+ * Submit a preimage to the bulletin chain.
173
+ * @param data - The data to submit.
174
+ * @returns The preimage key (hex string).
175
+ */
176
+ submit(data: Uint8Array): Promise<string>;
177
+
178
+ /**
179
+ * Look up a preimage by key.
180
+ * @param key - The preimage key (hex string).
181
+ * @param callback - Called with the data when found, or null if not yet available.
182
+ * @returns Subscription handle with unsubscribe method.
183
+ */
184
+ lookup(
185
+ key: string,
186
+ callback: (preimage: Uint8Array | null) => void,
187
+ ): { unsubscribe: () => void; onInterrupt: (cb: () => void) => () => void };
188
+ }
189
+
190
+ /**
191
+ * Get the accounts provider for managing host accounts.
192
+ *
193
+ * @returns The accounts provider, or `null` if unavailable.
194
+ */
195
+ export async function getAccountsProvider(): Promise<AccountsProvider | null> {
196
+ try {
197
+ const sdk = await import("@novasamatech/product-sdk");
198
+ return sdk.createAccountsProvider() as unknown as AccountsProvider;
199
+ } catch {
200
+ return null;
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Account from the host wallet.
206
+ */
207
+ export interface HostAccount {
208
+ publicKey: Uint8Array;
209
+ name?: string;
210
+ }
211
+
212
+ /**
213
+ * A product account — an app-scoped derived account managed by the host wallet.
214
+ *
215
+ * The host derives a unique keypair for each app (identified by `dotNsIdentifier`)
216
+ * so apps get their own account that the user controls but is scoped to the app.
217
+ */
218
+ export interface ProductAccount {
219
+ /** App identifier (e.g., "mark3t.dot"). */
220
+ dotNsIdentifier: string;
221
+ /** Derivation index within the app scope. Default: 0 */
222
+ derivationIndex: number;
223
+ /** Raw public key (32 bytes). */
224
+ publicKey: Uint8Array;
225
+ }
226
+
227
+ /**
228
+ * A contextual alias obtained from Ring VRF.
229
+ *
230
+ * Proves account membership in a ring without revealing which account.
231
+ */
232
+ export interface ContextualAlias {
233
+ /** Ring context (32 bytes). */
234
+ context: Uint8Array;
235
+ /** The Ring VRF alias bytes. */
236
+ alias: Uint8Array;
237
+ }
238
+
239
+ /**
240
+ * Neverthrow-style ResultAsync returned by product-sdk methods.
241
+ *
242
+ * Use `.match(onOk, onErr)` to handle success/error cases.
243
+ */
244
+ export interface ResultAsync<T, E> {
245
+ match: <A, B = A>(ok: (t: T) => A, err: (e: E) => B) => Promise<A | B>;
246
+ }
247
+
248
+ /**
249
+ * Accounts provider interface from @novasamatech/product-sdk.
250
+ *
251
+ * Provides methods for accessing host wallet accounts, product accounts,
252
+ * and Ring VRF operations.
253
+ */
254
+ export interface AccountsProvider {
255
+ /**
256
+ * Get non-product accounts (user's external wallets connected to the host).
257
+ *
258
+ * @returns ResultAsync resolving to array of accounts.
259
+ */
260
+ getNonProductAccounts: () => ResultAsync<HostAccount[], unknown>;
261
+
262
+ /**
263
+ * Get a signer for a non-product account.
264
+ *
265
+ * @param account - The product account (used for public key lookup).
266
+ * @returns A PolkadotSigner for signing transactions.
267
+ */
268
+ getNonProductAccountSigner: (account: ProductAccount) => import("polkadot-api").PolkadotSigner;
269
+
270
+ /**
271
+ * Get an app-scoped product account from the host.
272
+ *
273
+ * Product accounts are derived by the host wallet for each app, identified
274
+ * by `dotNsIdentifier` (e.g., "mark3t.dot"). The user controls these accounts
275
+ * but they are scoped to the requesting app.
276
+ *
277
+ * @param dotNsIdentifier - App identifier (e.g., "mark3t.dot").
278
+ * @param derivationIndex - Derivation index within the app scope. Default: 0
279
+ * @returns ResultAsync resolving to the account.
280
+ */
281
+ getProductAccount: (
282
+ dotNsIdentifier: string,
283
+ derivationIndex?: number,
284
+ ) => ResultAsync<HostAccount, unknown>;
285
+
286
+ /**
287
+ * Get a signer for a product account.
288
+ *
289
+ * @param account - The product account.
290
+ * @returns A PolkadotSigner for signing transactions.
291
+ */
292
+ getProductAccountSigner: (account: ProductAccount) => import("polkadot-api").PolkadotSigner;
293
+
294
+ /**
295
+ * Get a contextual alias for a product account via Ring VRF.
296
+ *
297
+ * Aliases prove account membership in a ring without revealing which
298
+ * account produced the alias.
299
+ *
300
+ * @param dotNsIdentifier - App identifier.
301
+ * @param derivationIndex - Derivation index. Default: 0
302
+ * @returns ResultAsync resolving to the contextual alias.
303
+ */
304
+ getProductAccountAlias: (
305
+ dotNsIdentifier: string,
306
+ derivationIndex?: number,
307
+ ) => ResultAsync<ContextualAlias, unknown>;
308
+
309
+ /**
310
+ * Create a Ring VRF proof for anonymous operations.
311
+ *
312
+ * Proves that the signer is a member of the ring at the given location
313
+ * without revealing which member.
314
+ *
315
+ * @param dotNsIdentifier - App identifier.
316
+ * @param derivationIndex - Derivation index.
317
+ * @param location - Ring location on-chain.
318
+ * @param message - Message to sign.
319
+ * @returns ResultAsync resolving to the proof bytes.
320
+ */
321
+ createRingVRFProof: (
322
+ dotNsIdentifier: string,
323
+ derivationIndex: number,
324
+ location: unknown,
325
+ message: Uint8Array,
326
+ ) => ResultAsync<Uint8Array, unknown>;
327
+
328
+ /**
329
+ * Subscribe to account connection status changes.
330
+ *
331
+ * @param callback - Called with status string ("connected" | "disconnected").
332
+ * @returns Unsubscribe handle.
333
+ */
334
+ subscribeAccountConnectionStatus: (
335
+ callback: (status: string) => void,
336
+ ) => { unsubscribe: () => void } | (() => void);
337
+ }
338
+
339
+ // ─────────────────────────────────────────────────────────────────────────────
340
+ // Tests
341
+ // ─────────────────────────────────────────────────────────────────────────────
342
+
343
+ if (import.meta.vitest) {
344
+ const { test, expect } = import.meta.vitest;
345
+
346
+ test("getTruApi returns TruApi when SDK is available", async () => {
347
+ // Reset cache for test
348
+ cachedTruApi = null;
349
+ const api = await getTruApi();
350
+ // In dev/test mode, product-sdk is installed
351
+ expect(api === null || typeof api === "object").toBe(true);
352
+ });
353
+
354
+ test("getPreimageManager returns manager when SDK is available", async () => {
355
+ const manager = await getPreimageManager();
356
+ // In dev/test mode, product-sdk is installed
357
+ expect(manager === null || typeof manager === "object").toBe(true);
358
+ });
359
+
360
+ test("getAccountsProvider returns provider when SDK is available", async () => {
361
+ // In dev/test mode, product-sdk is installed, so this returns a provider
362
+ const provider = await getAccountsProvider();
363
+ // Just verify it returns something (null when SDK unavailable, provider when available)
364
+ expect(provider === null || typeof provider === "object").toBe(true);
365
+ });
366
+
367
+ test("enumValue is exported", async () => {
368
+ const { enumValue } = await import("./truapi.js");
369
+ expect(typeof enumValue).toBe("function");
370
+ });
371
+ }
package/src/types.ts ADDED
@@ -0,0 +1,47 @@
1
+ /** Subset of product-sdk's hostLocalStorage that the KV store uses. */
2
+ export interface HostLocalStorage {
3
+ readString(key: string): Promise<string | null>;
4
+ writeString(key: string, value: string): Promise<void>;
5
+ readJSON<T>(key: string): Promise<T | null>;
6
+ writeJSON<T>(key: string, value: T): Promise<void>;
7
+ /**
8
+ * Clear a specific key from storage.
9
+ * @param key - The key to clear
10
+ */
11
+ clear(key: string): Promise<void>;
12
+ }
13
+
14
+ /** Proof types returned from createProof (matches product-sdk SCALE-decoded types). */
15
+ export type StatementProof =
16
+ | { tag: "Sr25519"; value: { signature: Uint8Array; signer: Uint8Array } }
17
+ | { tag: "Ed25519"; value: { signature: Uint8Array; signer: Uint8Array } }
18
+ | { tag: "Secp256k1Ecdsa"; value: { signature: Uint8Array; signer: Uint8Array } }
19
+ | { tag: "EcdsaRecoverable"; value: { signature: Uint8Array } };
20
+
21
+ /** The statement store interface provided by the host API via product-sdk. */
22
+ export interface HostStatementStore {
23
+ /**
24
+ * Subscribe to statements matching the given topics.
25
+ * @param topics - Topic filters as Uint8Array[]
26
+ * @param callback - Called with batches of SignedStatement[]
27
+ * @returns Subscription object with unsubscribe method
28
+ */
29
+ subscribe(
30
+ topics: Uint8Array[],
31
+ callback: (statements: unknown[]) => void,
32
+ ): { unsubscribe: () => void };
33
+
34
+ /**
35
+ * Create a proof for a statement using the given account.
36
+ * @param accountId - The account ID tuple [ss58Address, chainPrefix] from product-sdk
37
+ * @param statement - The unsigned statement
38
+ * @returns The proof (signature + signer info)
39
+ */
40
+ createProof(accountId: [string, number], statement: unknown): Promise<StatementProof>;
41
+
42
+ /**
43
+ * Submit a signed statement to the bulletin chain.
44
+ * @param signedStatement - Statement with attached proof
45
+ */
46
+ submit(signedStatement: unknown): Promise<void>;
47
+ }