@parity/product-sdk-host 0.4.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/dist/index.d.ts +380 -225
- package/dist/index.js +132 -32
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/chains.ts +2 -8
- package/src/chat.ts +124 -0
- package/src/container.ts +45 -9
- package/src/entropy.ts +65 -0
- package/src/index.ts +35 -4
- package/src/payments.ts +98 -0
- package/src/permissions.ts +124 -18
- package/src/theme.ts +82 -0
- package/src/truapi.ts +115 -155
- package/src/types.ts +62 -71
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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,19 +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/
|
|
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,
|
|
33
39
|
createProofAuthorized,
|
|
40
|
+
formatHostError,
|
|
34
41
|
// Helpers from @novasamatech/host-api
|
|
35
42
|
enumValue,
|
|
36
43
|
isEnumVariant,
|
|
@@ -56,8 +63,32 @@ export type {
|
|
|
56
63
|
AllocationOutcomeTag,
|
|
57
64
|
RemotePermission,
|
|
58
65
|
RemotePermissionTag,
|
|
59
|
-
Statement,
|
|
60
66
|
} from "./truapi.js";
|
|
61
67
|
|
|
62
|
-
// Higher-level permission
|
|
63
|
-
export { requestPermission } from "./permissions.js";
|
|
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";
|
package/src/payments.ts
ADDED
|
@@ -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
|
+
}
|
package/src/permissions.ts
CHANGED
|
@@ -1,22 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Higher-level
|
|
2
|
+
* Higher-level wrappers for the host's single-permission flows.
|
|
3
3
|
*
|
|
4
|
-
* `hostApi.permission`
|
|
5
|
-
* and
|
|
6
|
-
* Consumers rebuild that wrap/unwrap
|
|
7
|
-
*
|
|
8
|
-
* {@link
|
|
9
|
-
*
|
|
4
|
+
* `hostApi.permission` / `hostApi.devicePermission` take a versioned
|
|
5
|
+
* envelope (`enumValue("v1", ...)`) and return a neverthrow `ResultAsync`
|
|
6
|
+
* of an unwrapped versioned response. Consumers rebuild that wrap/unwrap
|
|
7
|
+
* dance every time. {@link requestPermission} and
|
|
8
|
+
* {@link requestDevicePermission} collapse it to one-liners that match the
|
|
9
|
+
* shape of {@link requestResourceAllocation} (throws on error, returns
|
|
10
|
+
* the unwrapped payload on success).
|
|
10
11
|
*
|
|
11
12
|
* @module
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
import { createLogger } from "@parity/product-sdk-logger";
|
|
15
16
|
|
|
16
|
-
import {
|
|
17
|
+
import type { CodecType } from "@novasamatech/host-api";
|
|
18
|
+
import type { DevicePermission as DevicePermissionCodec } from "@novasamatech/host-api";
|
|
19
|
+
|
|
20
|
+
import { enumValue, formatHostError, getTruApi, type RemotePermission } from "./truapi.js";
|
|
17
21
|
|
|
18
22
|
const log = createLogger("host:permissions");
|
|
19
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Device permission the dapp can ask the host to grant via
|
|
26
|
+
* {@link requestDevicePermission}.
|
|
27
|
+
*
|
|
28
|
+
* Derived from the upstream codec so variant renames surface as compile
|
|
29
|
+
* errors, not runtime failures.
|
|
30
|
+
*/
|
|
31
|
+
export type DevicePermissionKind = CodecType<typeof DevicePermissionCodec>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Alias of {@link RemotePermission} matching the upstream
|
|
35
|
+
* `@novasamatech/host-api-wrapper` name. Use either freely.
|
|
36
|
+
*/
|
|
37
|
+
export type RemotePermissionItem = RemotePermission;
|
|
38
|
+
|
|
20
39
|
/**
|
|
21
40
|
* Request a single remote permission from the host.
|
|
22
41
|
*
|
|
@@ -45,10 +64,43 @@ export async function requestPermission(permission: RemotePermission): Promise<b
|
|
|
45
64
|
return await truApi.permission(enumValue("v1", permission)).match(
|
|
46
65
|
(envelope: { tag: "v1"; value: boolean }) => envelope.value,
|
|
47
66
|
(err: unknown) => {
|
|
48
|
-
throw new Error(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
67
|
+
throw new Error(`requestPermission failed: ${formatHostError(err)}`, { cause: err });
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Request a single device permission (camera, microphone, etc.) from the
|
|
74
|
+
* host.
|
|
75
|
+
*
|
|
76
|
+
* Builds the `v1` envelope, calls `hostApi.devicePermission`, unwraps the
|
|
77
|
+
* response, and returns the host's boolean granted/denied outcome.
|
|
78
|
+
*
|
|
79
|
+
* @param permission - The device permission to request.
|
|
80
|
+
* @returns `true` if the host granted the permission, `false` if denied.
|
|
81
|
+
* @throws If the host is unavailable or the request fails.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* const granted = await requestDevicePermission("Camera");
|
|
86
|
+
* if (!granted) {
|
|
87
|
+
* showCameraDeniedMessage();
|
|
88
|
+
* }
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export async function requestDevicePermission(permission: DevicePermissionKind): Promise<boolean> {
|
|
92
|
+
const truApi = await getTruApi();
|
|
93
|
+
if (!truApi) {
|
|
94
|
+
throw new Error("requestDevicePermission: TruAPI unavailable");
|
|
95
|
+
}
|
|
96
|
+
log.debug("requestDevicePermission", { permission });
|
|
97
|
+
|
|
98
|
+
return await truApi.devicePermission(enumValue("v1", permission)).match(
|
|
99
|
+
(envelope: { tag: "v1"; value: boolean }) => envelope.value,
|
|
100
|
+
(err: unknown) => {
|
|
101
|
+
throw new Error(`requestDevicePermission failed: ${formatHostError(err)}`, {
|
|
102
|
+
cause: err,
|
|
103
|
+
});
|
|
52
104
|
},
|
|
53
105
|
);
|
|
54
106
|
}
|
|
@@ -57,14 +109,21 @@ if (import.meta.vitest) {
|
|
|
57
109
|
const { test, expect, describe, vi } = import.meta.vitest;
|
|
58
110
|
|
|
59
111
|
async function withMockedTruApi<T>(
|
|
60
|
-
bridge: {
|
|
112
|
+
bridge: {
|
|
113
|
+
permission?: (req: unknown) => unknown;
|
|
114
|
+
devicePermission?: (req: unknown) => unknown;
|
|
115
|
+
} | null,
|
|
61
116
|
fn: (mod: typeof import("./permissions.js")) => Promise<T>,
|
|
62
117
|
): Promise<T> {
|
|
63
118
|
vi.resetModules();
|
|
64
|
-
vi.doMock("./truapi.js", () =>
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
119
|
+
vi.doMock("./truapi.js", async (importOriginal) => {
|
|
120
|
+
const original = await importOriginal<typeof import("./truapi.js")>();
|
|
121
|
+
return {
|
|
122
|
+
...original,
|
|
123
|
+
getTruApi: async () => bridge,
|
|
124
|
+
enumValue: (version: string, value: unknown) => ({ tag: version, value }),
|
|
125
|
+
};
|
|
126
|
+
});
|
|
68
127
|
try {
|
|
69
128
|
const mod = await import("./permissions.js");
|
|
70
129
|
return await fn(mod);
|
|
@@ -118,7 +177,54 @@ if (import.meta.vitest) {
|
|
|
118
177
|
async (mod) => {
|
|
119
178
|
await expect(
|
|
120
179
|
mod.requestPermission({ tag: "ChainSubmit", value: undefined }),
|
|
121
|
-
).rejects.toThrow(/requestPermission failed/);
|
|
180
|
+
).rejects.toThrow(/requestPermission failed: GenericError: boom/);
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("requestDevicePermission", () => {
|
|
187
|
+
test("throws when TruAPI is unavailable", async () => {
|
|
188
|
+
await withMockedTruApi(null, async (mod) => {
|
|
189
|
+
await expect(mod.requestDevicePermission("Camera")).rejects.toThrow(
|
|
190
|
+
/TruAPI unavailable/,
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("unwraps the v1 boolean outcome", async () => {
|
|
196
|
+
await withMockedTruApi(
|
|
197
|
+
{
|
|
198
|
+
devicePermission: vi.fn().mockReturnValue({
|
|
199
|
+
match: async (onOk: (v: unknown) => unknown) =>
|
|
200
|
+
onOk({ tag: "v1", value: true }),
|
|
201
|
+
}),
|
|
202
|
+
},
|
|
203
|
+
async (mod) => {
|
|
204
|
+
const granted = await mod.requestDevicePermission("Camera");
|
|
205
|
+
expect(granted).toBe(true);
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("wraps host errors with a diagnostic message", async () => {
|
|
211
|
+
await withMockedTruApi(
|
|
212
|
+
{
|
|
213
|
+
devicePermission: vi.fn().mockReturnValue({
|
|
214
|
+
match: async (
|
|
215
|
+
_onOk: (v: unknown) => unknown,
|
|
216
|
+
onErr: (e: unknown) => unknown,
|
|
217
|
+
) =>
|
|
218
|
+
onErr({
|
|
219
|
+
tag: "v1",
|
|
220
|
+
value: { name: "GenericError", message: "boom" },
|
|
221
|
+
}),
|
|
222
|
+
}),
|
|
223
|
+
},
|
|
224
|
+
async (mod) => {
|
|
225
|
+
await expect(mod.requestDevicePermission("Camera")).rejects.toThrow(
|
|
226
|
+
/requestDevicePermission failed: GenericError: boom/,
|
|
227
|
+
);
|
|
122
228
|
},
|
|
123
229
|
);
|
|
124
230
|
});
|