@parity/product-sdk-host 0.12.0 → 0.14.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/errors.ts CHANGED
@@ -17,38 +17,44 @@
17
17
  *
18
18
  * @module
19
19
  */
20
- import type { GenericError } from "@parity/truapi";
20
+ import type { SdkError } from "@parity/product-sdk-errors";
21
+ import type { scale } from "@parity/truapi";
21
22
 
22
23
  /**
23
- * The structured error payload `@parity/truapi` surfaces on the `Err` channel of
24
- * a host call, once unwrapped from the versioned wire envelope. Every host error
25
- * union is built from these:
26
- *
27
- * - the catch-all {@link GenericError} (`{ reason }`),
28
- * - a unit tagged variant (`{ tag }`), or
29
- * - a tagged variant carrying a reason (`{ tag, value: { reason } }`).
30
- *
31
- * `GenericError` is imported from `@parity/truapi`; the `{ tag }` members are a
32
- * deliberate widening of truapi's per-domain named variants (the formatter is
33
- * tag-agnostic). truapi has no umbrella error union to import today — once it
34
- * exports a canonical tagged-error union from codegen, replace these local
35
- * members with that import so the type is protocol-sourced rather than
36
- * hand-widened.
24
+ * What a `Domain`-tagged call error carries. Widened from truapi's per-domain
25
+ * `Versioned*Error` types (all `{ tag: "V1", value: <domain error> }` today)
26
+ * so one payload type covers every call.
27
+ */
28
+ type VersionedDomainError = { tag: string; value?: unknown };
29
+
30
+ /**
31
+ * The error a host call puts on its `Err` channel — truapi's canonical
32
+ * {@link scale.CallErrorValue} envelope. `Denied` / `Unsupported` /
33
+ * `MalformedFrame` / `HostFailure` are transport-level failures; `Domain`
34
+ * wraps the actual per-domain error in a versioned envelope, which
35
+ * {@link formatHostError} digs through when rendering.
37
36
  *
38
- * This is the *payload* the host public API carries inside a
39
- * {@link HostCallFailedError} on the `err` channel of its `Result` returns — not
40
- * the error type consumers branch on.
37
+ * This is the payload {@link HostCallFailedError} carries not the error
38
+ * type consumers branch on.
41
39
  */
42
- export type HostErrorPayload =
43
- | GenericError
44
- | { tag: string; value?: undefined }
45
- | { tag: string; value: { reason: string } };
46
-
47
- /** Narrow an unknown `Err`-channel value to a {@link HostErrorPayload}. */
48
- function isHostErrorPayload(error: unknown): error is HostErrorPayload {
49
- if (error == null || typeof error !== "object") return false;
50
- const obj = error as Record<string, unknown>;
51
- return typeof obj.reason === "string" || typeof obj.tag === "string";
40
+ export type HostErrorPayload = scale.CallErrorValue<VersionedDomainError>;
41
+
42
+ /** Narrow to a tagged-union member: `{ tag, value? }`. */
43
+ function isTagged(value: unknown): value is { tag: string; value?: unknown } {
44
+ return (
45
+ value != null &&
46
+ typeof value === "object" &&
47
+ typeof (value as { tag?: unknown }).tag === "string"
48
+ );
49
+ }
50
+
51
+ /** Narrow to a reason-carrying payload — truapi's `GenericError` shape. */
52
+ function hasReason(value: unknown): value is { reason: string } {
53
+ return (
54
+ value != null &&
55
+ typeof value === "object" &&
56
+ typeof (value as { reason?: unknown }).reason === "string"
57
+ );
52
58
  }
53
59
 
54
60
  /**
@@ -66,16 +72,20 @@ export function formatHostError(error: unknown): string {
66
72
  if (error instanceof Error) return error.message;
67
73
  if (typeof error === "string") return error;
68
74
 
69
- if (isHostErrorPayload(error)) {
70
- if ("tag" in error) {
71
- // Tagged variant carrying a reason: { tag, value: { reason } }
72
- if (error.value != null && typeof error.value.reason === "string") {
73
- return `${error.tag}: ${error.value.reason}`;
74
- }
75
- // Unit tagged variant, e.g. { tag: "Full" } / { tag: "PermissionDenied" }
76
- return error.tag;
75
+ if (isTagged(error)) {
76
+ // `Domain` carries the real error inside a versioned envelope — unwrap it.
77
+ if (error.tag === "Domain" && isTagged(error.value) && error.value.value !== undefined) {
78
+ return formatHostError(error.value.value);
79
+ }
80
+ // Tagged variant carrying a reason: { tag, value: { reason } }
81
+ if (hasReason(error.value)) {
82
+ return `${error.tag}: ${error.value.reason}`;
77
83
  }
78
- // GenericError: { reason }
84
+ // Unit tagged variant, e.g. { tag: "Denied" } / { tag: "PermissionDenied" }
85
+ return error.tag;
86
+ }
87
+ // GenericError: { reason }
88
+ if (hasReason(error)) {
79
89
  return error.reason;
80
90
  }
81
91
 
@@ -93,9 +103,13 @@ export function formatHostError(error: unknown): string {
93
103
 
94
104
  /**
95
105
  * Base class for all host errors. Use `instanceof HostError` (or {@link isHostError})
96
- * to catch any host-related failure.
106
+ * to catch any host-related failure. Implements the cross-package
107
+ * {@link SdkError} marker so `isSdkError(e)` also recognizes it.
97
108
  */
98
- export class HostError extends Error {
109
+ export class HostError extends Error implements SdkError {
110
+ readonly isSdkError = true as const;
111
+ readonly source = "host";
112
+
99
113
  constructor(message: string, options?: ErrorOptions) {
100
114
  super(message, options);
101
115
  this.name = "HostError";
@@ -156,7 +170,13 @@ if (import.meta.vitest) {
156
170
  });
157
171
 
158
172
  test("HostCallFailedError renders payload and preserves it", () => {
159
- const payload = { tag: "PermissionDenied", value: { reason: "user said no" } };
173
+ const payload: HostErrorPayload = {
174
+ tag: "Domain",
175
+ value: {
176
+ tag: "V1",
177
+ value: { tag: "PermissionDenied", value: { reason: "user said no" } },
178
+ },
179
+ };
160
180
  const e = new HostCallFailedError("requestPermission failed", payload);
161
181
  expect(e).toBeInstanceOf(HostError);
162
182
  expect(e.payload).toBe(payload);
@@ -164,14 +184,17 @@ if (import.meta.vitest) {
164
184
  expect(e.message).toBe("requestPermission failed: PermissionDenied: user said no");
165
185
  });
166
186
 
167
- test("HostCallFailedError renders a GenericError payload", () => {
168
- const e = new HostCallFailedError("submit failed", { reason: "timeout" });
187
+ test("HostCallFailedError renders a Domain-wrapped GenericError payload", () => {
188
+ const e = new HostCallFailedError("submit failed", {
189
+ tag: "Domain",
190
+ value: { tag: "V1", value: { reason: "timeout" } },
191
+ });
169
192
  expect(e.message).toBe("submit failed: timeout");
170
193
  });
171
194
 
172
195
  test("isHostError narrows host errors only", () => {
173
196
  expect(isHostError(new HostUnavailableError())).toBe(true);
174
- expect(isHostError(new HostCallFailedError("x", { reason: "y" }))).toBe(true);
197
+ expect(isHostError(new HostCallFailedError("x", { tag: "Denied" }))).toBe(true);
175
198
  expect(isHostError(new Error("plain"))).toBe(false);
176
199
  expect(isHostError("string")).toBe(false);
177
200
  });
@@ -189,6 +212,24 @@ if (import.meta.vitest) {
189
212
  expect(formatHostError({ tag: "Full" })).toBe("Full");
190
213
  });
191
214
 
215
+ test("unwraps the CallError Domain envelope to the domain error", () => {
216
+ // { tag: "Domain", value: { tag: "V1", value: <domain error> } }
217
+ expect(
218
+ formatHostError({
219
+ tag: "Domain",
220
+ value: { tag: "V1", value: { tag: "PermissionDenied" } },
221
+ }),
222
+ ).toBe("PermissionDenied");
223
+ expect(
224
+ formatHostError({ tag: "Domain", value: { tag: "V1", value: { reason: "boom" } } }),
225
+ ).toBe("boom");
226
+ // Transport-level CallError variants render as-is.
227
+ expect(formatHostError({ tag: "Denied" })).toBe("Denied");
228
+ expect(formatHostError({ tag: "HostFailure", value: { reason: "crashed" } })).toBe(
229
+ "HostFailure: crashed",
230
+ );
231
+ });
232
+
192
233
  test("falls back for non-host-error input", () => {
193
234
  expect(formatHostError(new Error("plain"))).toBe("plain");
194
235
  expect(formatHostError("string err")).toBe("string err");
package/src/index.ts CHANGED
@@ -56,6 +56,7 @@ export type {
56
56
  // Result type + typed host errors (the throw→Result boundary)
57
57
  export { ok, err } from "./result.js";
58
58
  export type { Result } from "./result.js";
59
+ export { type SdkError, isSdkError } from "@parity/product-sdk-errors";
59
60
  export {
60
61
  HostError,
61
62
  HostUnavailableError,
@@ -72,7 +73,9 @@ export type {
72
73
  HostAccount,
73
74
  ProductAccount,
74
75
  ContextualAlias,
76
+ ProductProofContext,
75
77
  RingLocation,
78
+ RingVRFProof,
76
79
  } from "./accounts.js";
77
80
 
78
81
  // Higher-level permission wrappers
package/src/payments.ts CHANGED
@@ -5,16 +5,17 @@
5
5
  * `truApi.payment.*`.
6
6
  *
7
7
  * Exposes balance subscription, top-up, payment requests, and payment-status
8
- * subscription. Distinct from the CoinPayment / merchant-payments surface
9
- * (RFC-0017): RFC-0006 is the user-initiated balance / top-up / payment-request
10
- * flow.
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).
11
12
  *
12
13
  * @module
13
14
  */
14
15
 
15
16
  import type {
16
17
  Balance,
17
- PaymentPurseId,
18
+ CoinPaymentPurseId,
18
19
  HexString,
19
20
  HostPaymentBalanceSubscribeItem,
20
21
  HostPaymentStatusSubscribeItem,
@@ -37,13 +38,13 @@ import type { HostSubscription } from "./types.js";
37
38
  export interface PaymentManager {
38
39
  subscribeBalance(
39
40
  callback: (balance: HostPaymentBalanceSubscribeItem) => void,
40
- purse?: PaymentPurseId,
41
+ purse?: CoinPaymentPurseId,
41
42
  ): HostSubscription;
42
- topUp(amount: Balance, source: PaymentTopUpSource, into?: PaymentPurseId): Promise<void>;
43
+ topUp(amount: Balance, source: PaymentTopUpSource, into?: CoinPaymentPurseId): Promise<void>;
43
44
  requestPayment(
44
45
  amount: Balance,
45
46
  destination: HexString,
46
- from?: PaymentPurseId,
47
+ from?: CoinPaymentPurseId,
47
48
  ): Promise<{ id: string }>;
48
49
  subscribePaymentStatus(
49
50
  paymentId: string,
package/src/result.ts CHANGED
@@ -1,56 +1,13 @@
1
1
  // Copyright 2026 Parity Technologies (UK) Ltd.
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  /**
4
- * A lightweight tagged `Result` type for the host public API.
4
+ * Re-export of the shared `Result` primitive from `@parity/result`.
5
5
  *
6
6
  * Host functions return `Promise<Result<T, HostError>>` rather than throwing, so
7
- * consumers get typed errors on the `err` channel instead of opaque thrown
8
- * `Error`s. The shape is intentionally identical to the one
9
- * `@parity/product-sdk-signer` exposes (`{ ok: true; value } | { ok: false; error }`),
10
- * so the two layers compose with no adapter — host's `Result` flows straight into
11
- * the signer's pattern matching.
12
- *
13
- * NOTE: host owns its own copy because the dependency edge runs `signer → host`,
14
- * so host cannot import the signer's definition. If a third package ever needs
15
- * this shape, extract it into a shared `@parity/product-sdk-result` package and
16
- * have both depend on that instead of duplicating.
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.
17
10
  *
18
11
  * @module
19
12
  */
20
-
21
- /** A value that is either a success (`ok`) carrying `T`, or a failure (`err`) carrying `E`. */
22
- export type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
23
-
24
- /** Create a successful {@link Result}. */
25
- export function ok<T>(value: T): Result<T, never> {
26
- return { ok: true, value };
27
- }
28
-
29
- /** Create a failed {@link Result}. */
30
- export function err<E>(error: E): Result<never, E> {
31
- return { ok: false, error };
32
- }
33
-
34
- if (import.meta.vitest) {
35
- const { test, expect, describe } = import.meta.vitest;
36
-
37
- describe("ok", () => {
38
- test("produces an ok result with value", () => {
39
- const result = ok(42);
40
- expect(result.ok).toBe(true);
41
- expect(result).toEqual({ ok: true, value: 42 });
42
- });
43
-
44
- test("works with null value", () => {
45
- expect(ok(null)).toEqual({ ok: true, value: null });
46
- });
47
- });
48
-
49
- describe("err", () => {
50
- test("produces an error result", () => {
51
- const result = err("boom");
52
- expect(result.ok).toBe(false);
53
- expect(result).toEqual({ ok: false, error: "boom" });
54
- });
55
- });
56
- }
13
+ export { type Result, ok, err } from "@parity/result";