@parity/product-sdk-host 0.0.0-dev.312.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,117 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Wrapper for the host's payment manager (RFC-0006), backed by
5
+ * `truApi.payment.*`.
6
+ *
7
+ * Exposes balance subscription, top-up, payment requests, and payment-status
8
+ * subscription. The flow is distinct from the CoinPayment / merchant-payments
9
+ * surface (RFC-0017) — RFC-0006 is the user-initiated balance / top-up /
10
+ * payment-request flow — but both operate on the same CoinPayment purses,
11
+ * hence the shared `CoinPaymentPurseId` (omitted = the main purse).
12
+ *
13
+ * @module
14
+ */
15
+
16
+ import type {
17
+ Balance,
18
+ CoinPaymentPurseId,
19
+ HexString,
20
+ HostPaymentBalanceSubscribeItem,
21
+ HostPaymentStatusSubscribeItem,
22
+ PaymentTopUpSource,
23
+ TrUApiClient,
24
+ } from "@parity/truapi";
25
+
26
+ import { getClient, subscribeWithInterrupt } from "./transport.js";
27
+ import { unwrapHostResult } from "./truapi.js";
28
+ import type { HostSubscription } from "./types.js";
29
+
30
+ /**
31
+ * Payment manager handle. Exposes balance subscription, top-up, payment
32
+ * requests, and payment-status subscription.
33
+ *
34
+ * The balance / status / top-up-source shapes are `@parity/truapi`'s
35
+ * `HostPaymentBalanceSubscribeItem`, `HostPaymentStatusSubscribeItem`, and
36
+ * `PaymentTopUpSource` — used directly rather than re-aliased.
37
+ */
38
+ export interface PaymentManager {
39
+ subscribeBalance(
40
+ callback: (balance: HostPaymentBalanceSubscribeItem) => void,
41
+ purse?: CoinPaymentPurseId,
42
+ ): HostSubscription;
43
+ topUp(amount: Balance, source: PaymentTopUpSource, into?: CoinPaymentPurseId): Promise<void>;
44
+ requestPayment(
45
+ amount: Balance,
46
+ destination: HexString,
47
+ from?: CoinPaymentPurseId,
48
+ ): Promise<{ id: string }>;
49
+ subscribePaymentStatus(
50
+ paymentId: string,
51
+ callback: (status: HostPaymentStatusSubscribeItem) => void,
52
+ ): HostSubscription;
53
+ }
54
+
55
+ /** Build a {@link PaymentManager} over a TruAPI client's `payment` domain. */
56
+ function adaptPaymentManager(client: TrUApiClient): PaymentManager {
57
+ const payment = client.payment;
58
+ return {
59
+ subscribeBalance(callback, purse) {
60
+ return subscribeWithInterrupt(
61
+ payment.balanceSubscribe({ request: { purse } }),
62
+ callback,
63
+ );
64
+ },
65
+ topUp(amount, source, into) {
66
+ return unwrapHostResult(
67
+ payment.topUp({ into, amount, source }),
68
+ "payment topUp failed",
69
+ );
70
+ },
71
+ async requestPayment(amount, destination, from) {
72
+ const response = await unwrapHostResult(
73
+ payment.request({ from, amount, destination }),
74
+ "payment requestPayment failed",
75
+ );
76
+ return { id: response.id };
77
+ },
78
+ subscribePaymentStatus(paymentId, callback) {
79
+ return subscribeWithInterrupt(
80
+ payment.statusSubscribe({ request: { paymentId } }),
81
+ callback,
82
+ );
83
+ },
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Get the host payment manager, backed by `truApi.payment.*`. Returns `null`
89
+ * when running outside a host container.
90
+ *
91
+ * @returns The payment manager, or `null` if unavailable.
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * import { getPaymentManager } from "@parity/product-sdk-host";
96
+ *
97
+ * const payments = await getPaymentManager();
98
+ * if (payments) {
99
+ * const sub = payments.subscribeBalance((b) => { ... });
100
+ * await payments.topUp(1_000_000n, { tag: "ProductAccount", value: { derivationIndex: { tag: "Index", value: 0 } } });
101
+ * const { id } = await payments.requestPayment(500n, "0x…");
102
+ * sub.unsubscribe();
103
+ * }
104
+ * ```
105
+ */
106
+ export async function getPaymentManager(): Promise<PaymentManager | null> {
107
+ const client = await getClient();
108
+ return client ? adaptPaymentManager(client) : null;
109
+ }
110
+
111
+ if (import.meta.vitest) {
112
+ const { test, expect } = import.meta.vitest;
113
+
114
+ test("getPaymentManager returns null outside a container", async () => {
115
+ expect(await getPaymentManager()).toBeNull();
116
+ });
117
+ }
@@ -0,0 +1,236 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Higher-level wrappers for the host's single-permission flows.
5
+ *
6
+ * `truApi.permissions.requestRemotePermission` / `requestDevicePermission`
7
+ * return a neverthrow `ResultAsync` of a `{ granted }` response.
8
+ * {@link requestPermission} and {@link requestDevicePermission} collapse that
9
+ * to one-liners returning a `Result<boolean, HostError>` — the granted flag on
10
+ * success, a typed {@link HostError} on the `err` channel.
11
+ *
12
+ * @module
13
+ */
14
+
15
+ import type { HostDevicePermissionRequest } from "@parity/truapi";
16
+ import { createLogger } from "@parity/product-sdk-logger";
17
+
18
+ import { type HostError, HostUnavailableError } from "./errors.js";
19
+ import { type Result, err } from "./result.js";
20
+ import { getTruApi, mapHostResult, type RemotePermission } from "./truapi.js";
21
+
22
+ const log = createLogger("host:permissions");
23
+
24
+ /**
25
+ * Device permission the dapp can ask the host to grant via
26
+ * {@link requestDevicePermission}. A string union (`"Camera"`, `"Microphone"`,
27
+ * …) re-exported from `@parity/truapi`.
28
+ */
29
+ export type DevicePermissionKind = HostDevicePermissionRequest;
30
+
31
+ /**
32
+ * Legacy alias of {@link RemotePermission}, kept for back-compat with code that
33
+ * used the older name. Use either freely.
34
+ */
35
+ export type RemotePermissionItem = RemotePermission;
36
+
37
+ /**
38
+ * Request a single remote permission from the host.
39
+ *
40
+ * Calls `truApi.permissions.requestRemotePermission` and returns the host's
41
+ * boolean granted/denied outcome.
42
+ *
43
+ * @param permission - The remote permission to request.
44
+ * @returns `ok(true)` if the host granted the permission, `ok(false)` if denied,
45
+ * or `err(HostUnavailableError | HostCallFailedError)`.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * const r = await requestPermission({ tag: "ChainSubmit", value: undefined });
50
+ * if (!r.ok || !r.value) {
51
+ * tellUserToReconnect();
52
+ * }
53
+ * ```
54
+ */
55
+ export async function requestPermission(
56
+ permission: RemotePermission,
57
+ ): Promise<Result<boolean, HostError>> {
58
+ const truApi = await getTruApi();
59
+ if (!truApi) {
60
+ return err(new HostUnavailableError("requestPermission: TruAPI unavailable"));
61
+ }
62
+ log.debug("requestPermission", { tag: permission.tag });
63
+
64
+ return mapHostResult(
65
+ truApi.permissions.requestRemotePermission({ permission }),
66
+ (response) => response.granted,
67
+ "requestPermission failed",
68
+ );
69
+ }
70
+
71
+ /**
72
+ * Request a single device permission (camera, microphone, etc.) from the
73
+ * host.
74
+ *
75
+ * Calls `truApi.permissions.requestDevicePermission` and returns the host's
76
+ * boolean granted/denied outcome.
77
+ *
78
+ * @param permission - The device permission to request.
79
+ * @returns `ok(true)` if the host granted the permission, `ok(false)` if denied,
80
+ * or `err(HostUnavailableError | HostCallFailedError)`.
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * const r = await requestDevicePermission("Camera");
85
+ * if (!r.ok || !r.value) {
86
+ * showCameraDeniedMessage();
87
+ * }
88
+ * ```
89
+ */
90
+ export async function requestDevicePermission(
91
+ permission: DevicePermissionKind,
92
+ ): Promise<Result<boolean, HostError>> {
93
+ const truApi = await getTruApi();
94
+ if (!truApi) {
95
+ return err(new HostUnavailableError("requestDevicePermission: TruAPI unavailable"));
96
+ }
97
+ log.debug("requestDevicePermission", { permission });
98
+
99
+ return mapHostResult(
100
+ truApi.permissions.requestDevicePermission(permission),
101
+ (response) => response.granted,
102
+ "requestDevicePermission failed",
103
+ );
104
+ }
105
+
106
+ if (import.meta.vitest) {
107
+ const { test, expect, describe, vi } = import.meta.vitest;
108
+
109
+ function okAsync<T>(value: T) {
110
+ return { match: async (onOk: (v: T) => unknown) => onOk(value) };
111
+ }
112
+ function errAsync<E>(error: E) {
113
+ return {
114
+ match: async (_onOk: (v: unknown) => unknown, onErr: (e: E) => unknown) => onErr(error),
115
+ };
116
+ }
117
+
118
+ async function withMockedTruApi<T>(
119
+ client: unknown,
120
+ fn: (mod: typeof import("./permissions.js")) => Promise<T>,
121
+ ): Promise<T> {
122
+ vi.resetModules();
123
+ vi.doMock("./truapi.js", async (importOriginal) => {
124
+ const original = await importOriginal<typeof import("./truapi.js")>();
125
+ return { ...original, getTruApi: async () => client };
126
+ });
127
+ try {
128
+ const mod = await import("./permissions.js");
129
+ return await fn(mod);
130
+ } finally {
131
+ vi.doUnmock("./truapi.js");
132
+ vi.resetModules();
133
+ }
134
+ }
135
+
136
+ describe("requestPermission", () => {
137
+ test("returns err(HostUnavailableError) when TruAPI is unavailable", async () => {
138
+ await withMockedTruApi(null, async (mod) => {
139
+ const result = await mod.requestPermission({
140
+ tag: "ChainSubmit",
141
+ value: undefined,
142
+ });
143
+ expect(result.ok).toBe(false);
144
+ if (!result.ok) {
145
+ expect(result.error.name).toBe("HostUnavailableError");
146
+ }
147
+ });
148
+ });
149
+
150
+ test("returns ok with the granted flag", async () => {
151
+ await withMockedTruApi(
152
+ {
153
+ permissions: {
154
+ requestRemotePermission: vi.fn(() => okAsync({ granted: true })),
155
+ },
156
+ },
157
+ async (mod) => {
158
+ const result = await mod.requestPermission({
159
+ tag: "ChainSubmit",
160
+ value: undefined,
161
+ });
162
+ expect(result).toEqual({ ok: true, value: true });
163
+ },
164
+ );
165
+ });
166
+
167
+ test("wraps host errors in err(HostCallFailedError) with a diagnostic message", async () => {
168
+ await withMockedTruApi(
169
+ {
170
+ permissions: {
171
+ requestRemotePermission: vi.fn(() => errAsync({ reason: "boom" })),
172
+ },
173
+ },
174
+ async (mod) => {
175
+ const result = await mod.requestPermission({
176
+ tag: "ChainSubmit",
177
+ value: undefined,
178
+ });
179
+ expect(result.ok).toBe(false);
180
+ if (!result.ok) {
181
+ expect(result.error.name).toBe("HostCallFailedError");
182
+ expect(result.error.message).toMatch(/requestPermission failed: boom/);
183
+ }
184
+ },
185
+ );
186
+ });
187
+ });
188
+
189
+ describe("requestDevicePermission", () => {
190
+ test("returns err(HostUnavailableError) when TruAPI is unavailable", async () => {
191
+ await withMockedTruApi(null, async (mod) => {
192
+ const result = await mod.requestDevicePermission("Camera");
193
+ expect(result.ok).toBe(false);
194
+ if (!result.ok) {
195
+ expect(result.error.name).toBe("HostUnavailableError");
196
+ }
197
+ });
198
+ });
199
+
200
+ test("returns ok with the granted flag", async () => {
201
+ await withMockedTruApi(
202
+ {
203
+ permissions: {
204
+ requestDevicePermission: vi.fn(() => okAsync({ granted: true })),
205
+ },
206
+ },
207
+ async (mod) => {
208
+ expect(await mod.requestDevicePermission("Camera")).toEqual({
209
+ ok: true,
210
+ value: true,
211
+ });
212
+ },
213
+ );
214
+ });
215
+
216
+ test("wraps host errors in err(HostCallFailedError) with a diagnostic message", async () => {
217
+ await withMockedTruApi(
218
+ {
219
+ permissions: {
220
+ requestDevicePermission: vi.fn(() => errAsync({ reason: "boom" })),
221
+ },
222
+ },
223
+ async (mod) => {
224
+ const result = await mod.requestDevicePermission("Camera");
225
+ expect(result.ok).toBe(false);
226
+ if (!result.ok) {
227
+ expect(result.error.name).toBe("HostCallFailedError");
228
+ expect(result.error.message).toMatch(
229
+ /requestDevicePermission failed: boom/,
230
+ );
231
+ }
232
+ },
233
+ );
234
+ });
235
+ });
236
+ }
package/src/result.ts ADDED
@@ -0,0 +1,13 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Re-export of the shared `Result` primitive from `@parity/result`.
5
+ *
6
+ * Host functions return `Promise<Result<T, HostError>>` rather than throwing, so
7
+ * consumers get typed errors on the `err` channel. The type is now owned by the
8
+ * zero-dependency `@parity/result` leaf so every package shares one
9
+ * definition; this module stays as the host-internal import path.
10
+ *
11
+ * @module
12
+ */
13
+ export { type Result, ok, err } from "@parity/result";