@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/LICENSE +201 -0
- package/dist/index.d.ts +337 -0
- package/dist/index.js +219 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
- package/src/chains.ts +34 -0
- package/src/container.ts +178 -0
- package/src/index.ts +35 -0
- package/src/truapi.ts +371 -0
- package/src/types.ts +47 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// src/container.ts
|
|
2
|
+
async function isInsideContainer() {
|
|
3
|
+
if (typeof window === "undefined") return false;
|
|
4
|
+
try {
|
|
5
|
+
const sdk = await import("@novasamatech/product-sdk");
|
|
6
|
+
return sdk.sandboxProvider.isCorrectEnvironment();
|
|
7
|
+
} catch {
|
|
8
|
+
return isInsideContainerSync();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
async function getHostLocalStorage() {
|
|
12
|
+
if (!await isInsideContainer()) return null;
|
|
13
|
+
try {
|
|
14
|
+
const sdk = await import("@novasamatech/product-sdk");
|
|
15
|
+
return sdk.hostLocalStorage;
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function getHostProvider(genesisHash) {
|
|
21
|
+
try {
|
|
22
|
+
const sdk = await import("@novasamatech/product-sdk");
|
|
23
|
+
return sdk.createPapiProvider(genesisHash);
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function isInsideContainerSync() {
|
|
29
|
+
if (typeof window === "undefined") return false;
|
|
30
|
+
const win = window;
|
|
31
|
+
try {
|
|
32
|
+
if (window !== window.top) return true;
|
|
33
|
+
} catch {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
if (win.__HOST_WEBVIEW_MARK__ === true) return true;
|
|
37
|
+
if (win.__HOST_API_PORT__ != null) return true;
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
async function getStatementStore() {
|
|
41
|
+
try {
|
|
42
|
+
const sdk = await import("@novasamatech/product-sdk");
|
|
43
|
+
return sdk.createStatementStore();
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (void 0) {
|
|
49
|
+
const { test, expect, vi } = void 0;
|
|
50
|
+
test("returns false in Node environment (no window)", async () => {
|
|
51
|
+
expect(await isInsideContainer()).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
test("manualDetection returns true for __HOST_WEBVIEW_MARK__", async () => {
|
|
54
|
+
const fakeWindow = {
|
|
55
|
+
top: null,
|
|
56
|
+
__HOST_WEBVIEW_MARK__: true
|
|
57
|
+
};
|
|
58
|
+
vi.stubGlobal("window", fakeWindow);
|
|
59
|
+
const result = await isInsideContainer();
|
|
60
|
+
expect(result).toBe(true);
|
|
61
|
+
vi.unstubAllGlobals();
|
|
62
|
+
});
|
|
63
|
+
test("manualDetection returns true for __HOST_API_PORT__", async () => {
|
|
64
|
+
const fakeWindow = {
|
|
65
|
+
top: null,
|
|
66
|
+
__HOST_API_PORT__: 12345
|
|
67
|
+
};
|
|
68
|
+
vi.stubGlobal("window", fakeWindow);
|
|
69
|
+
const result = await isInsideContainer();
|
|
70
|
+
expect(result).toBe(true);
|
|
71
|
+
vi.unstubAllGlobals();
|
|
72
|
+
});
|
|
73
|
+
test("manualDetection returns false when no signals present", async () => {
|
|
74
|
+
const fakeWindow = { top: null };
|
|
75
|
+
Object.defineProperty(fakeWindow, "top", { get: () => fakeWindow });
|
|
76
|
+
vi.stubGlobal("window", fakeWindow);
|
|
77
|
+
const result = await isInsideContainer();
|
|
78
|
+
expect(result).toBe(false);
|
|
79
|
+
vi.unstubAllGlobals();
|
|
80
|
+
});
|
|
81
|
+
test("manualDetection returns true for cross-origin iframe", async () => {
|
|
82
|
+
const fakeWindow = {};
|
|
83
|
+
Object.defineProperty(fakeWindow, "top", {
|
|
84
|
+
get: () => {
|
|
85
|
+
throw new DOMException("cross-origin");
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
vi.stubGlobal("window", fakeWindow);
|
|
89
|
+
const result = await isInsideContainer();
|
|
90
|
+
expect(result).toBe(true);
|
|
91
|
+
vi.unstubAllGlobals();
|
|
92
|
+
});
|
|
93
|
+
test("manualDetection returns true when window !== window.top (iframe)", async () => {
|
|
94
|
+
const fakeWindow = { top: {} };
|
|
95
|
+
vi.stubGlobal("window", fakeWindow);
|
|
96
|
+
const result = await isInsideContainer();
|
|
97
|
+
expect(result).toBe(true);
|
|
98
|
+
vi.unstubAllGlobals();
|
|
99
|
+
});
|
|
100
|
+
test("getHostLocalStorage returns null outside container", async () => {
|
|
101
|
+
expect(await getHostLocalStorage()).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
test("getHostProvider returns null when product-sdk unavailable", async () => {
|
|
104
|
+
const result = await getHostProvider("0xabc");
|
|
105
|
+
expect(result).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
test("getStatementStore returns null when product-sdk unavailable", async () => {
|
|
108
|
+
const result = await getStatementStore();
|
|
109
|
+
expect(result).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/chains.ts
|
|
114
|
+
var BULLETIN_RPCS = {
|
|
115
|
+
paseo: ["wss://paseo-bulletin-rpc.polkadot.io"],
|
|
116
|
+
polkadot: [],
|
|
117
|
+
kusama: []
|
|
118
|
+
};
|
|
119
|
+
var DEFAULT_BULLETIN_ENDPOINT = BULLETIN_RPCS.paseo[0];
|
|
120
|
+
if (void 0) {
|
|
121
|
+
const { describe, test, expect } = void 0;
|
|
122
|
+
describe("chains config", () => {
|
|
123
|
+
test("BULLETIN_RPCS has paseo endpoint", () => {
|
|
124
|
+
expect(BULLETIN_RPCS.paseo.length).toBeGreaterThan(0);
|
|
125
|
+
expect(BULLETIN_RPCS.paseo[0]).toMatch(/^wss:\/\//);
|
|
126
|
+
});
|
|
127
|
+
test("BULLETIN_RPCS polkadot and kusama are empty until live", () => {
|
|
128
|
+
expect(BULLETIN_RPCS.polkadot).toEqual([]);
|
|
129
|
+
expect(BULLETIN_RPCS.kusama).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
test("DEFAULT_BULLETIN_ENDPOINT matches first paseo endpoint", () => {
|
|
132
|
+
expect(DEFAULT_BULLETIN_ENDPOINT).toBe(BULLETIN_RPCS.paseo[0]);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/truapi.ts
|
|
138
|
+
import { createLogger } from "@parity/product-sdk-logger";
|
|
139
|
+
import {
|
|
140
|
+
enumValue,
|
|
141
|
+
isEnumVariant,
|
|
142
|
+
assertEnumVariant,
|
|
143
|
+
unwrapResultOrThrow,
|
|
144
|
+
resultOk,
|
|
145
|
+
resultErr,
|
|
146
|
+
toHex,
|
|
147
|
+
fromHex
|
|
148
|
+
} from "@novasamatech/host-api";
|
|
149
|
+
var log = createLogger("host");
|
|
150
|
+
var cachedTruApi = null;
|
|
151
|
+
async function getTruApi() {
|
|
152
|
+
if (cachedTruApi) return cachedTruApi;
|
|
153
|
+
try {
|
|
154
|
+
const sdk = await import("@novasamatech/product-sdk");
|
|
155
|
+
cachedTruApi = sdk.hostApi;
|
|
156
|
+
log.debug("TruAPI loaded");
|
|
157
|
+
return cachedTruApi;
|
|
158
|
+
} catch {
|
|
159
|
+
log.debug("TruAPI unavailable (not in container or SDK not installed)");
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function getPreimageManager() {
|
|
164
|
+
try {
|
|
165
|
+
const sdk = await import("@novasamatech/product-sdk");
|
|
166
|
+
return sdk.preimageManager;
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async function getAccountsProvider() {
|
|
172
|
+
try {
|
|
173
|
+
const sdk = await import("@novasamatech/product-sdk");
|
|
174
|
+
return sdk.createAccountsProvider();
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (void 0) {
|
|
180
|
+
const { test, expect } = void 0;
|
|
181
|
+
test("getTruApi returns TruApi when SDK is available", async () => {
|
|
182
|
+
cachedTruApi = null;
|
|
183
|
+
const api = await getTruApi();
|
|
184
|
+
expect(api === null || typeof api === "object").toBe(true);
|
|
185
|
+
});
|
|
186
|
+
test("getPreimageManager returns manager when SDK is available", async () => {
|
|
187
|
+
const manager = await getPreimageManager();
|
|
188
|
+
expect(manager === null || typeof manager === "object").toBe(true);
|
|
189
|
+
});
|
|
190
|
+
test("getAccountsProvider returns provider when SDK is available", async () => {
|
|
191
|
+
const provider = await getAccountsProvider();
|
|
192
|
+
expect(provider === null || typeof provider === "object").toBe(true);
|
|
193
|
+
});
|
|
194
|
+
test("enumValue is exported", async () => {
|
|
195
|
+
const { enumValue: enumValue2 } = await null;
|
|
196
|
+
expect(typeof enumValue2).toBe("function");
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
export {
|
|
200
|
+
BULLETIN_RPCS,
|
|
201
|
+
DEFAULT_BULLETIN_ENDPOINT,
|
|
202
|
+
assertEnumVariant,
|
|
203
|
+
enumValue,
|
|
204
|
+
fromHex,
|
|
205
|
+
getAccountsProvider,
|
|
206
|
+
getHostLocalStorage,
|
|
207
|
+
getHostProvider,
|
|
208
|
+
getPreimageManager,
|
|
209
|
+
getStatementStore,
|
|
210
|
+
getTruApi,
|
|
211
|
+
isEnumVariant,
|
|
212
|
+
isInsideContainer,
|
|
213
|
+
isInsideContainerSync,
|
|
214
|
+
resultErr,
|
|
215
|
+
resultOk,
|
|
216
|
+
toHex,
|
|
217
|
+
unwrapResultOrThrow
|
|
218
|
+
};
|
|
219
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/container.ts","../src/chains.ts","../src/truapi.ts"],"sourcesContent":["import type { JsonRpcProvider } from \"polkadot-api/ws-provider/web\";\n\nimport type { HostLocalStorage, HostStatementStore } from \"./types.js\";\n\n/**\n * Detect if running inside a Host container (Polkadot Browser / Polkadot Desktop).\n *\n * The SDK is designed to run exclusively inside a host container. This function\n * is primarily useful for early validation or informational purposes.\n *\n * Uses product-sdk's sandboxProvider as primary detection.\n * Falls back to manual signal checks when product-sdk is not installed.\n */\nexport async function isInsideContainer(): Promise<boolean> {\n if (typeof window === \"undefined\") return false;\n\n try {\n const sdk = await import(\"@novasamatech/product-sdk\");\n return sdk.sandboxProvider.isCorrectEnvironment();\n } catch {\n return isInsideContainerSync();\n }\n}\n\n/**\n * Get the Host API localStorage instance when running inside a container.\n * Returns null outside a container or when product-sdk is unavailable.\n */\nexport async function getHostLocalStorage(): Promise<HostLocalStorage | null> {\n if (!(await isInsideContainer())) return null;\n\n try {\n const sdk = await import(\"@novasamatech/product-sdk\");\n return sdk.hostLocalStorage as HostLocalStorage;\n } catch {\n return null;\n }\n}\n\n/**\n * Get a PAPI-compatible JSON-RPC provider that routes through the host connection.\n *\n * When running inside a Polkadot container, this wraps the chain connection via the\n * host's `createPapiProvider`, enabling shared connections and efficient routing.\n * Returns `null` when `@novasamatech/product-sdk` is unavailable.\n *\n * @param genesisHash - Genesis hash of the target chain (`0x`-prefixed hex string).\n * @returns A host-routed `JsonRpcProvider`, or `null` if unavailable.\n */\nexport async function getHostProvider(genesisHash: `0x${string}`): Promise<JsonRpcProvider | null> {\n try {\n const sdk = await import(\"@novasamatech/product-sdk\");\n return sdk.createPapiProvider(genesisHash);\n } catch {\n return null;\n }\n}\n\n/**\n * Synchronous container detection — fast heuristic check without product-sdk.\n *\n * Checks for iframe, webview marker, and host message port signals.\n * Use this when you need a quick sync check (e.g., in hot code paths).\n * For full detection including product-sdk, use {@link isInsideContainer} (async).\n */\nexport function isInsideContainerSync(): boolean {\n if (typeof window === \"undefined\") return false;\n\n const win = window as unknown as Record<string, unknown>;\n\n // Iframe detection (polkadot.com browser)\n try {\n if (window !== window.top) return true;\n } catch {\n // Cross-origin iframe — likely inside a container\n return true;\n }\n\n // Webview detection (Polkadot Desktop)\n if (win.__HOST_WEBVIEW_MARK__ === true) return true;\n\n // Desktop message-passing API\n if (win.__HOST_API_PORT__ != null) return true;\n\n return false;\n}\n\n/**\n * Get the host API statement store when running inside a container.\n *\n * Returns a statement store with `subscribe`, `createProof`, and `submit` methods\n * that communicate through the host's native binary protocol — bypassing JSON-RPC\n * entirely. Returns `null` when `@novasamatech/product-sdk` is unavailable.\n *\n * @returns The host statement store, or `null` if unavailable.\n */\nexport async function getStatementStore(): Promise<HostStatementStore | null> {\n try {\n const sdk = await import(\"@novasamatech/product-sdk\");\n return sdk.createStatementStore() as HostStatementStore;\n } catch {\n return null;\n }\n}\n\nif (import.meta.vitest) {\n const { test, expect, vi } = import.meta.vitest;\n\n test(\"returns false in Node environment (no window)\", async () => {\n expect(await isInsideContainer()).toBe(false);\n });\n\n test(\"manualDetection returns true for __HOST_WEBVIEW_MARK__\", async () => {\n const fakeWindow = {\n top: null,\n __HOST_WEBVIEW_MARK__: true,\n };\n vi.stubGlobal(\"window\", fakeWindow);\n const result = await isInsideContainer();\n expect(result).toBe(true);\n vi.unstubAllGlobals();\n });\n\n test(\"manualDetection returns true for __HOST_API_PORT__\", async () => {\n const fakeWindow = {\n top: null,\n __HOST_API_PORT__: 12345,\n };\n vi.stubGlobal(\"window\", fakeWindow);\n const result = await isInsideContainer();\n expect(result).toBe(true);\n vi.unstubAllGlobals();\n });\n\n test(\"manualDetection returns false when no signals present\", async () => {\n const fakeWindow = { top: null };\n Object.defineProperty(fakeWindow, \"top\", { get: () => fakeWindow });\n vi.stubGlobal(\"window\", fakeWindow);\n const result = await isInsideContainer();\n expect(result).toBe(false);\n vi.unstubAllGlobals();\n });\n\n test(\"manualDetection returns true for cross-origin iframe\", async () => {\n const fakeWindow = {};\n Object.defineProperty(fakeWindow, \"top\", {\n get: () => {\n throw new DOMException(\"cross-origin\");\n },\n });\n vi.stubGlobal(\"window\", fakeWindow);\n const result = await isInsideContainer();\n expect(result).toBe(true);\n vi.unstubAllGlobals();\n });\n\n test(\"manualDetection returns true when window !== window.top (iframe)\", async () => {\n const fakeWindow = { top: {} }; // top is a different object\n vi.stubGlobal(\"window\", fakeWindow);\n const result = await isInsideContainer();\n expect(result).toBe(true);\n vi.unstubAllGlobals();\n });\n\n test(\"getHostLocalStorage returns null outside container\", async () => {\n expect(await getHostLocalStorage()).toBeNull();\n });\n\n test(\"getHostProvider returns null when product-sdk unavailable\", async () => {\n const result = await getHostProvider(\"0xabc\");\n expect(result).toBeNull();\n });\n\n test(\"getStatementStore returns null when product-sdk unavailable\", async () => {\n const result = await getStatementStore();\n expect(result).toBeNull();\n });\n}\n","/**\n * Shared chain network configuration — single source of truth for\n * chain-specific endpoints used by multiple packages.\n */\n\n/** Bulletin chain RPC endpoints by environment. */\nexport const BULLETIN_RPCS = {\n paseo: [\"wss://paseo-bulletin-rpc.polkadot.io\"],\n polkadot: [] as string[],\n kusama: [] as string[],\n} as const;\n\n/** Default bulletin endpoint (first paseo endpoint). */\nexport const DEFAULT_BULLETIN_ENDPOINT: string = BULLETIN_RPCS.paseo[0];\n\nif (import.meta.vitest) {\n const { describe, test, expect } = import.meta.vitest;\n\n describe(\"chains config\", () => {\n test(\"BULLETIN_RPCS has paseo endpoint\", () => {\n expect(BULLETIN_RPCS.paseo.length).toBeGreaterThan(0);\n expect(BULLETIN_RPCS.paseo[0]).toMatch(/^wss:\\/\\//);\n });\n\n test(\"BULLETIN_RPCS polkadot and kusama are empty until live\", () => {\n expect(BULLETIN_RPCS.polkadot).toEqual([]);\n expect(BULLETIN_RPCS.kusama).toEqual([]);\n });\n\n test(\"DEFAULT_BULLETIN_ENDPOINT matches first paseo endpoint\", () => {\n expect(DEFAULT_BULLETIN_ENDPOINT).toBe(BULLETIN_RPCS.paseo[0]);\n });\n });\n}\n","/**\n * TruAPI - the protocol for communicating between apps and the Polkadot host container.\n *\n * This module centralizes access to @novasamatech/product-sdk and @novasamatech/host-api,\n * allowing other @parity/product-sdk-* packages to import from here rather than depending\n * directly on novasama packages.\n *\n * @module\n */\n\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nconst log = createLogger(\"host\");\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Helpers from @novasamatech/host-api (re-exported from @novasamatech/scale)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport {\n /**\n * Construct an enum variant for TruAPI calls.\n *\n * @example\n * ```ts\n * import { enumValue, getTruApi } from \"@parity/product-sdk-host\";\n *\n * const truApi = await getTruApi();\n * if (truApi) {\n * await truApi.permission([enumValue(\"ChainSubmit\")]);\n * }\n * ```\n */\n enumValue,\n /**\n * Check if a value is a specific enum variant.\n */\n isEnumVariant,\n /**\n * Assert that a value is a specific enum variant, throwing if not.\n */\n assertEnumVariant,\n /**\n * Unwrap a Result, throwing on error.\n */\n unwrapResultOrThrow,\n /**\n * Create an Ok result.\n */\n resultOk,\n /**\n * Create an Err result.\n */\n resultErr,\n /**\n * Convert bytes to hex string.\n */\n toHex,\n /**\n * Convert hex string to bytes.\n */\n fromHex,\n} from \"@novasamatech/host-api\";\n\nexport type { HexString } from \"@novasamatech/host-api\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// TruAPI accessor\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The TruApi type - provides low-level methods for communicating with the host.\n *\n * Methods include:\n * - `navigateTo(url)` — Navigate to a URL within the host\n * - `permission(permissions)` — Request permissions from the host\n * - `localStorageRead/Write/Clear` — Host-backed storage\n * - `sign(payload)` — Request transaction signing\n * - `deriveEntropy(context)` — Derive deterministic entropy\n * - `themeSubscribe()` — Subscribe to host theme changes\n * - And many more...\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type TruApi = any;\n\n/** Cached TruApi instance */\nlet cachedTruApi: TruApi | null = null;\n\n/**\n * Get the TruAPI instance for direct low-level access.\n *\n * Returns the `hostApi` object from `@novasamatech/product-sdk` which provides\n * methods for communicating directly with the host container. Returns `null`\n * when running outside a container or when the SDK is unavailable.\n *\n * For most use cases, prefer the higher-level functions like `getHostLocalStorage()`,\n * `getHostProvider()`, etc. Use this when you need direct access to host methods\n * like `navigateTo()`, `permission()`, or `deriveEntropy()`.\n *\n * @example\n * ```ts\n * import { getTruApi, enumValue } from \"@parity/product-sdk-host\";\n *\n * const truApi = await getTruApi();\n * if (truApi) {\n * // Request permission\n * const result = await truApi.permission([enumValue(\"ChainSubmit\")]);\n *\n * // Navigate to a URL\n * await truApi.navigateTo(\"polkadot://settings\");\n *\n * // Subscribe to theme changes\n * const sub = truApi.themeSubscribe(undefined, (theme) => {\n * console.log(\"Theme changed:\", theme);\n * });\n * }\n * ```\n *\n * @returns The TruAPI instance, or `null` if unavailable.\n */\nexport async function getTruApi(): Promise<TruApi | null> {\n if (cachedTruApi) return cachedTruApi;\n\n try {\n const sdk = await import(\"@novasamatech/product-sdk\");\n cachedTruApi = sdk.hostApi;\n log.debug(\"TruAPI loaded\");\n return cachedTruApi;\n } catch {\n log.debug(\"TruAPI unavailable (not in container or SDK not installed)\");\n return null;\n }\n}\n\n/**\n * Get the preimage manager for bulletin chain operations.\n *\n * The preimage manager handles uploading and looking up preimages (arbitrary data)\n * on the bulletin chain through the host's optimized path.\n *\n * @returns The preimage manager, or `null` if unavailable.\n *\n * @example\n * ```ts\n * import { getPreimageManager } from \"@parity/product-sdk-host\";\n *\n * const manager = await getPreimageManager();\n * if (manager) {\n * // Submit a preimage\n * const key = await manager.submit(new Uint8Array([1, 2, 3]));\n *\n * // Look up a preimage\n * const sub = manager.lookup(key, (data) => {\n * if (data) console.log(\"Found:\", data);\n * });\n * }\n * ```\n */\nexport async function getPreimageManager(): Promise<PreimageManager | null> {\n try {\n const sdk = await import(\"@novasamatech/product-sdk\");\n return sdk.preimageManager;\n } catch {\n return null;\n }\n}\n\n/**\n * Preimage manager interface for bulletin chain operations.\n */\nexport interface PreimageManager {\n /**\n * Submit a preimage to the bulletin chain.\n * @param data - The data to submit.\n * @returns The preimage key (hex string).\n */\n submit(data: Uint8Array): Promise<string>;\n\n /**\n * Look up a preimage by key.\n * @param key - The preimage key (hex string).\n * @param callback - Called with the data when found, or null if not yet available.\n * @returns Subscription handle with unsubscribe method.\n */\n lookup(\n key: string,\n callback: (preimage: Uint8Array | null) => void,\n ): { unsubscribe: () => void; onInterrupt: (cb: () => void) => () => void };\n}\n\n/**\n * Get the accounts provider for managing host accounts.\n *\n * @returns The accounts provider, or `null` if unavailable.\n */\nexport async function getAccountsProvider(): Promise<AccountsProvider | null> {\n try {\n const sdk = await import(\"@novasamatech/product-sdk\");\n return sdk.createAccountsProvider() as unknown as AccountsProvider;\n } catch {\n return null;\n }\n}\n\n/**\n * Account from the host wallet.\n */\nexport interface HostAccount {\n publicKey: Uint8Array;\n name?: string;\n}\n\n/**\n * A product account — an app-scoped derived account managed by the host wallet.\n *\n * The host derives a unique keypair for each app (identified by `dotNsIdentifier`)\n * so apps get their own account that the user controls but is scoped to the app.\n */\nexport interface ProductAccount {\n /** App identifier (e.g., \"mark3t.dot\"). */\n dotNsIdentifier: string;\n /** Derivation index within the app scope. Default: 0 */\n derivationIndex: number;\n /** Raw public key (32 bytes). */\n publicKey: Uint8Array;\n}\n\n/**\n * A contextual alias obtained from Ring VRF.\n *\n * Proves account membership in a ring without revealing which account.\n */\nexport interface ContextualAlias {\n /** Ring context (32 bytes). */\n context: Uint8Array;\n /** The Ring VRF alias bytes. */\n alias: Uint8Array;\n}\n\n/**\n * Neverthrow-style ResultAsync returned by product-sdk methods.\n *\n * Use `.match(onOk, onErr)` to handle success/error cases.\n */\nexport interface ResultAsync<T, E> {\n match: <A, B = A>(ok: (t: T) => A, err: (e: E) => B) => Promise<A | B>;\n}\n\n/**\n * Accounts provider interface from @novasamatech/product-sdk.\n *\n * Provides methods for accessing host wallet accounts, product accounts,\n * and Ring VRF operations.\n */\nexport interface AccountsProvider {\n /**\n * Get non-product accounts (user's external wallets connected to the host).\n *\n * @returns ResultAsync resolving to array of accounts.\n */\n getNonProductAccounts: () => ResultAsync<HostAccount[], unknown>;\n\n /**\n * Get a signer for a non-product account.\n *\n * @param account - The product account (used for public key lookup).\n * @returns A PolkadotSigner for signing transactions.\n */\n getNonProductAccountSigner: (account: ProductAccount) => import(\"polkadot-api\").PolkadotSigner;\n\n /**\n * Get an app-scoped product account from the host.\n *\n * Product accounts are derived by the host wallet for each app, identified\n * by `dotNsIdentifier` (e.g., \"mark3t.dot\"). The user controls these accounts\n * but they are scoped to the requesting app.\n *\n * @param dotNsIdentifier - App identifier (e.g., \"mark3t.dot\").\n * @param derivationIndex - Derivation index within the app scope. Default: 0\n * @returns ResultAsync resolving to the account.\n */\n getProductAccount: (\n dotNsIdentifier: string,\n derivationIndex?: number,\n ) => ResultAsync<HostAccount, unknown>;\n\n /**\n * Get a signer for a product account.\n *\n * @param account - The product account.\n * @returns A PolkadotSigner for signing transactions.\n */\n getProductAccountSigner: (account: ProductAccount) => import(\"polkadot-api\").PolkadotSigner;\n\n /**\n * Get a contextual alias for a product account via Ring VRF.\n *\n * Aliases prove account membership in a ring without revealing which\n * account produced the alias.\n *\n * @param dotNsIdentifier - App identifier.\n * @param derivationIndex - Derivation index. Default: 0\n * @returns ResultAsync resolving to the contextual alias.\n */\n getProductAccountAlias: (\n dotNsIdentifier: string,\n derivationIndex?: number,\n ) => ResultAsync<ContextualAlias, unknown>;\n\n /**\n * Create a Ring VRF proof for anonymous operations.\n *\n * Proves that the signer is a member of the ring at the given location\n * without revealing which member.\n *\n * @param dotNsIdentifier - App identifier.\n * @param derivationIndex - Derivation index.\n * @param location - Ring location on-chain.\n * @param message - Message to sign.\n * @returns ResultAsync resolving to the proof bytes.\n */\n createRingVRFProof: (\n dotNsIdentifier: string,\n derivationIndex: number,\n location: unknown,\n message: Uint8Array,\n ) => ResultAsync<Uint8Array, unknown>;\n\n /**\n * Subscribe to account connection status changes.\n *\n * @param callback - Called with status string (\"connected\" | \"disconnected\").\n * @returns Unsubscribe handle.\n */\n subscribeAccountConnectionStatus: (\n callback: (status: string) => void,\n ) => { unsubscribe: () => void } | (() => void);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Tests\n// ─────────────────────────────────────────────────────────────────────────────\n\nif (import.meta.vitest) {\n const { test, expect } = import.meta.vitest;\n\n test(\"getTruApi returns TruApi when SDK is available\", async () => {\n // Reset cache for test\n cachedTruApi = null;\n const api = await getTruApi();\n // In dev/test mode, product-sdk is installed\n expect(api === null || typeof api === \"object\").toBe(true);\n });\n\n test(\"getPreimageManager returns manager when SDK is available\", async () => {\n const manager = await getPreimageManager();\n // In dev/test mode, product-sdk is installed\n expect(manager === null || typeof manager === \"object\").toBe(true);\n });\n\n test(\"getAccountsProvider returns provider when SDK is available\", async () => {\n // In dev/test mode, product-sdk is installed, so this returns a provider\n const provider = await getAccountsProvider();\n // Just verify it returns something (null when SDK unavailable, provider when available)\n expect(provider === null || typeof provider === \"object\").toBe(true);\n });\n\n test(\"enumValue is exported\", async () => {\n const { enumValue } = await import(\"./truapi.js\");\n expect(typeof enumValue).toBe(\"function\");\n });\n}\n"],"mappings":"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","names":["enumValue"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@parity/product-sdk-host",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Host container detection and storage access for Polkadot Desktop and Mobile environments",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"polkadot-api": "^1.9.0",
|
|
22
|
+
"@parity/product-sdk-logger": "0.1.0"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@novasamatech/product-sdk": ">=0.1.0",
|
|
26
|
+
"@novasamatech/host-api": ">=0.6.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependenciesMeta": {
|
|
29
|
+
"@novasamatech/product-sdk": {
|
|
30
|
+
"optional": true
|
|
31
|
+
},
|
|
32
|
+
"@novasamatech/host-api": {
|
|
33
|
+
"optional": true
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@novasamatech/product-sdk": "^0.6.17",
|
|
38
|
+
"@novasamatech/host-api": "^0.7.0",
|
|
39
|
+
"typescript": "^5.7.0",
|
|
40
|
+
"vitest": "^3.0.0"
|
|
41
|
+
},
|
|
42
|
+
"license": "Apache-2.0",
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup",
|
|
45
|
+
"test": "vitest",
|
|
46
|
+
"clean": "rm -rf dist"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/chains.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared chain network configuration — single source of truth for
|
|
3
|
+
* chain-specific endpoints used by multiple packages.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Bulletin chain RPC endpoints by environment. */
|
|
7
|
+
export const BULLETIN_RPCS = {
|
|
8
|
+
paseo: ["wss://paseo-bulletin-rpc.polkadot.io"],
|
|
9
|
+
polkadot: [] as string[],
|
|
10
|
+
kusama: [] as string[],
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
/** Default bulletin endpoint (first paseo endpoint). */
|
|
14
|
+
export const DEFAULT_BULLETIN_ENDPOINT: string = BULLETIN_RPCS.paseo[0];
|
|
15
|
+
|
|
16
|
+
if (import.meta.vitest) {
|
|
17
|
+
const { describe, test, expect } = import.meta.vitest;
|
|
18
|
+
|
|
19
|
+
describe("chains config", () => {
|
|
20
|
+
test("BULLETIN_RPCS has paseo endpoint", () => {
|
|
21
|
+
expect(BULLETIN_RPCS.paseo.length).toBeGreaterThan(0);
|
|
22
|
+
expect(BULLETIN_RPCS.paseo[0]).toMatch(/^wss:\/\//);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("BULLETIN_RPCS polkadot and kusama are empty until live", () => {
|
|
26
|
+
expect(BULLETIN_RPCS.polkadot).toEqual([]);
|
|
27
|
+
expect(BULLETIN_RPCS.kusama).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("DEFAULT_BULLETIN_ENDPOINT matches first paseo endpoint", () => {
|
|
31
|
+
expect(DEFAULT_BULLETIN_ENDPOINT).toBe(BULLETIN_RPCS.paseo[0]);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
package/src/container.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { JsonRpcProvider } from "polkadot-api/ws-provider/web";
|
|
2
|
+
|
|
3
|
+
import type { HostLocalStorage, HostStatementStore } from "./types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detect if running inside a Host container (Polkadot Browser / Polkadot Desktop).
|
|
7
|
+
*
|
|
8
|
+
* The SDK is designed to run exclusively inside a host container. This function
|
|
9
|
+
* is primarily useful for early validation or informational purposes.
|
|
10
|
+
*
|
|
11
|
+
* Uses product-sdk's sandboxProvider as primary detection.
|
|
12
|
+
* Falls back to manual signal checks when product-sdk is not installed.
|
|
13
|
+
*/
|
|
14
|
+
export async function isInsideContainer(): Promise<boolean> {
|
|
15
|
+
if (typeof window === "undefined") return false;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const sdk = await import("@novasamatech/product-sdk");
|
|
19
|
+
return sdk.sandboxProvider.isCorrectEnvironment();
|
|
20
|
+
} catch {
|
|
21
|
+
return isInsideContainerSync();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get the Host API localStorage instance when running inside a container.
|
|
27
|
+
* Returns null outside a container or when product-sdk is unavailable.
|
|
28
|
+
*/
|
|
29
|
+
export async function getHostLocalStorage(): Promise<HostLocalStorage | null> {
|
|
30
|
+
if (!(await isInsideContainer())) return null;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const sdk = await import("@novasamatech/product-sdk");
|
|
34
|
+
return sdk.hostLocalStorage as HostLocalStorage;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get a PAPI-compatible JSON-RPC provider that routes through the host connection.
|
|
42
|
+
*
|
|
43
|
+
* When running inside a Polkadot container, this wraps the chain connection via the
|
|
44
|
+
* host's `createPapiProvider`, enabling shared connections and efficient routing.
|
|
45
|
+
* Returns `null` when `@novasamatech/product-sdk` is unavailable.
|
|
46
|
+
*
|
|
47
|
+
* @param genesisHash - Genesis hash of the target chain (`0x`-prefixed hex string).
|
|
48
|
+
* @returns A host-routed `JsonRpcProvider`, or `null` if unavailable.
|
|
49
|
+
*/
|
|
50
|
+
export async function getHostProvider(genesisHash: `0x${string}`): Promise<JsonRpcProvider | null> {
|
|
51
|
+
try {
|
|
52
|
+
const sdk = await import("@novasamatech/product-sdk");
|
|
53
|
+
return sdk.createPapiProvider(genesisHash);
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Synchronous container detection — fast heuristic check without product-sdk.
|
|
61
|
+
*
|
|
62
|
+
* Checks for iframe, webview marker, and host message port signals.
|
|
63
|
+
* Use this when you need a quick sync check (e.g., in hot code paths).
|
|
64
|
+
* For full detection including product-sdk, use {@link isInsideContainer} (async).
|
|
65
|
+
*/
|
|
66
|
+
export function isInsideContainerSync(): boolean {
|
|
67
|
+
if (typeof window === "undefined") return false;
|
|
68
|
+
|
|
69
|
+
const win = window as unknown as Record<string, unknown>;
|
|
70
|
+
|
|
71
|
+
// Iframe detection (polkadot.com browser)
|
|
72
|
+
try {
|
|
73
|
+
if (window !== window.top) return true;
|
|
74
|
+
} catch {
|
|
75
|
+
// Cross-origin iframe — likely inside a container
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Webview detection (Polkadot Desktop)
|
|
80
|
+
if (win.__HOST_WEBVIEW_MARK__ === true) return true;
|
|
81
|
+
|
|
82
|
+
// Desktop message-passing API
|
|
83
|
+
if (win.__HOST_API_PORT__ != null) return true;
|
|
84
|
+
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get the host API statement store when running inside a container.
|
|
90
|
+
*
|
|
91
|
+
* Returns a statement store with `subscribe`, `createProof`, and `submit` methods
|
|
92
|
+
* that communicate through the host's native binary protocol — bypassing JSON-RPC
|
|
93
|
+
* entirely. Returns `null` when `@novasamatech/product-sdk` is unavailable.
|
|
94
|
+
*
|
|
95
|
+
* @returns The host statement store, or `null` if unavailable.
|
|
96
|
+
*/
|
|
97
|
+
export async function getStatementStore(): Promise<HostStatementStore | null> {
|
|
98
|
+
try {
|
|
99
|
+
const sdk = await import("@novasamatech/product-sdk");
|
|
100
|
+
return sdk.createStatementStore() as HostStatementStore;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (import.meta.vitest) {
|
|
107
|
+
const { test, expect, vi } = import.meta.vitest;
|
|
108
|
+
|
|
109
|
+
test("returns false in Node environment (no window)", async () => {
|
|
110
|
+
expect(await isInsideContainer()).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("manualDetection returns true for __HOST_WEBVIEW_MARK__", async () => {
|
|
114
|
+
const fakeWindow = {
|
|
115
|
+
top: null,
|
|
116
|
+
__HOST_WEBVIEW_MARK__: true,
|
|
117
|
+
};
|
|
118
|
+
vi.stubGlobal("window", fakeWindow);
|
|
119
|
+
const result = await isInsideContainer();
|
|
120
|
+
expect(result).toBe(true);
|
|
121
|
+
vi.unstubAllGlobals();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("manualDetection returns true for __HOST_API_PORT__", async () => {
|
|
125
|
+
const fakeWindow = {
|
|
126
|
+
top: null,
|
|
127
|
+
__HOST_API_PORT__: 12345,
|
|
128
|
+
};
|
|
129
|
+
vi.stubGlobal("window", fakeWindow);
|
|
130
|
+
const result = await isInsideContainer();
|
|
131
|
+
expect(result).toBe(true);
|
|
132
|
+
vi.unstubAllGlobals();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("manualDetection returns false when no signals present", async () => {
|
|
136
|
+
const fakeWindow = { top: null };
|
|
137
|
+
Object.defineProperty(fakeWindow, "top", { get: () => fakeWindow });
|
|
138
|
+
vi.stubGlobal("window", fakeWindow);
|
|
139
|
+
const result = await isInsideContainer();
|
|
140
|
+
expect(result).toBe(false);
|
|
141
|
+
vi.unstubAllGlobals();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("manualDetection returns true for cross-origin iframe", async () => {
|
|
145
|
+
const fakeWindow = {};
|
|
146
|
+
Object.defineProperty(fakeWindow, "top", {
|
|
147
|
+
get: () => {
|
|
148
|
+
throw new DOMException("cross-origin");
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
vi.stubGlobal("window", fakeWindow);
|
|
152
|
+
const result = await isInsideContainer();
|
|
153
|
+
expect(result).toBe(true);
|
|
154
|
+
vi.unstubAllGlobals();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("manualDetection returns true when window !== window.top (iframe)", async () => {
|
|
158
|
+
const fakeWindow = { top: {} }; // top is a different object
|
|
159
|
+
vi.stubGlobal("window", fakeWindow);
|
|
160
|
+
const result = await isInsideContainer();
|
|
161
|
+
expect(result).toBe(true);
|
|
162
|
+
vi.unstubAllGlobals();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("getHostLocalStorage returns null outside container", async () => {
|
|
166
|
+
expect(await getHostLocalStorage()).toBeNull();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("getHostProvider returns null when product-sdk unavailable", async () => {
|
|
170
|
+
const result = await getHostProvider("0xabc");
|
|
171
|
+
expect(result).toBeNull();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("getStatementStore returns null when product-sdk unavailable", async () => {
|
|
175
|
+
const result = await getStatementStore();
|
|
176
|
+
expect(result).toBeNull();
|
|
177
|
+
});
|
|
178
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export {
|
|
2
|
+
isInsideContainer,
|
|
3
|
+
isInsideContainerSync,
|
|
4
|
+
getHostLocalStorage,
|
|
5
|
+
getHostProvider,
|
|
6
|
+
getStatementStore,
|
|
7
|
+
} from "./container.js";
|
|
8
|
+
export type { HostLocalStorage, HostStatementStore, StatementProof } from "./types.js";
|
|
9
|
+
export { BULLETIN_RPCS, DEFAULT_BULLETIN_ENDPOINT } from "./chains.js";
|
|
10
|
+
|
|
11
|
+
// TruAPI - re-exports from @novasamatech/product-sdk and @novasamatech/host-api
|
|
12
|
+
export {
|
|
13
|
+
getTruApi,
|
|
14
|
+
getPreimageManager,
|
|
15
|
+
getAccountsProvider,
|
|
16
|
+
// Helpers from @novasamatech/host-api
|
|
17
|
+
enumValue,
|
|
18
|
+
isEnumVariant,
|
|
19
|
+
assertEnumVariant,
|
|
20
|
+
unwrapResultOrThrow,
|
|
21
|
+
resultOk,
|
|
22
|
+
resultErr,
|
|
23
|
+
toHex,
|
|
24
|
+
fromHex,
|
|
25
|
+
} from "./truapi.js";
|
|
26
|
+
export type {
|
|
27
|
+
TruApi,
|
|
28
|
+
HexString,
|
|
29
|
+
PreimageManager,
|
|
30
|
+
AccountsProvider,
|
|
31
|
+
HostAccount,
|
|
32
|
+
ProductAccount,
|
|
33
|
+
ContextualAlias,
|
|
34
|
+
ResultAsync,
|
|
35
|
+
} from "./truapi.js";
|