@parity/product-sdk-host 0.11.0 → 0.12.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 +528 -534
- package/dist/index.js +853 -285
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/src/accounts.ts +544 -0
- package/src/chain-spec.ts +126 -84
- package/src/chain-transaction.ts +107 -78
- package/src/chat.ts +81 -85
- package/src/container.ts +211 -246
- package/src/entropy.ts +63 -25
- package/src/errors.ts +198 -0
- package/src/features.ts +66 -55
- package/src/index.ts +33 -22
- package/src/navigation.ts +50 -49
- package/src/notifications.ts +59 -69
- package/src/papi-provider.ts +673 -0
- package/src/payments.ts +77 -61
- package/src/permissions.ts +107 -105
- package/src/result.ts +56 -0
- package/src/theme.ts +35 -63
- package/src/transport.ts +71 -0
- package/src/truapi.ts +166 -409
- package/src/types.ts +69 -61
package/src/entropy.ts
CHANGED
|
@@ -3,18 +3,19 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Higher-level wrapper for the host's entropy derivation (RFC-0007).
|
|
5
5
|
*
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* {@link requestPermission} and {@link requestResourceAllocation}.
|
|
6
|
+
* `truApi.entropy.derive` takes a hex `context` and returns a hex `entropy`
|
|
7
|
+
* payload wrapped in a neverthrow `ResultAsync`. `deriveEntropy` keeps the
|
|
8
|
+
* ergonomic `Uint8Array → Result<Uint8Array, HostError>` signature: it
|
|
9
|
+
* hex-encodes the context on the way in and decodes the entropy on the way out.
|
|
11
10
|
*
|
|
12
11
|
* @module
|
|
13
12
|
*/
|
|
14
13
|
|
|
15
14
|
import { createLogger } from "@parity/product-sdk-logger";
|
|
16
15
|
|
|
17
|
-
import {
|
|
16
|
+
import { type HostError, HostUnavailableError } from "./errors.js";
|
|
17
|
+
import { type Result, err } from "./result.js";
|
|
18
|
+
import { fromHex, getTruApi, mapHostResult, toHex } from "./truapi.js";
|
|
18
19
|
|
|
19
20
|
const log = createLogger("host:entropy");
|
|
20
21
|
|
|
@@ -26,42 +27,79 @@ const log = createLogger("host:entropy");
|
|
|
26
27
|
* different keys (or different wallets) yield uncorrelated entropy.
|
|
27
28
|
*
|
|
28
29
|
* @param key - Context key bytes (typically a SCALE-encoded discriminator).
|
|
29
|
-
* @returns
|
|
30
|
-
*
|
|
30
|
+
* @returns `ok` with the derived entropy bytes, or
|
|
31
|
+
* `err(HostUnavailableError | HostCallFailedError)`.
|
|
31
32
|
*
|
|
32
33
|
* @example
|
|
33
34
|
* ```ts
|
|
34
35
|
* import { deriveEntropy } from "@parity/product-sdk-host";
|
|
35
36
|
*
|
|
36
|
-
* const
|
|
37
|
+
* const r = await deriveEntropy(new TextEncoder().encode("my-app:seed-v1"));
|
|
38
|
+
* if (r.ok) { const seed = r.value; }
|
|
37
39
|
* ```
|
|
38
40
|
*/
|
|
39
|
-
export async function deriveEntropy(key: Uint8Array): Promise<Uint8Array
|
|
41
|
+
export async function deriveEntropy(key: Uint8Array): Promise<Result<Uint8Array, HostError>> {
|
|
40
42
|
const truApi = await getTruApi();
|
|
41
43
|
if (!truApi) {
|
|
42
|
-
|
|
44
|
+
return err(new HostUnavailableError("deriveEntropy: TruAPI unavailable"));
|
|
43
45
|
}
|
|
44
46
|
log.debug("deriveEntropy", { keyLen: key.length });
|
|
45
47
|
|
|
46
|
-
return
|
|
47
|
-
(
|
|
48
|
-
(
|
|
49
|
-
|
|
50
|
-
},
|
|
48
|
+
return mapHostResult(
|
|
49
|
+
truApi.entropy.derive({ context: toHex(key) }),
|
|
50
|
+
(response) => fromHex(response.entropy),
|
|
51
|
+
"deriveEntropy failed",
|
|
51
52
|
);
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
if (import.meta.vitest) {
|
|
55
|
-
const { test, expect } = import.meta.vitest;
|
|
56
|
+
const { test, expect, describe, vi } = import.meta.vitest;
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
function okAsync<T>(value: T) {
|
|
59
|
+
return { match: async (onOk: (v: T) => unknown) => onOk(value) };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function withMockedTruApi<T>(
|
|
63
|
+
client: unknown,
|
|
64
|
+
fn: (mod: typeof import("./entropy.js")) => Promise<T>,
|
|
65
|
+
): Promise<T> {
|
|
66
|
+
vi.resetModules();
|
|
67
|
+
vi.doMock("./truapi.js", async (importOriginal) => {
|
|
68
|
+
const original = await importOriginal<typeof import("./truapi.js")>();
|
|
69
|
+
return { ...original, getTruApi: async () => client };
|
|
70
|
+
});
|
|
71
|
+
try {
|
|
72
|
+
const mod = await import("./entropy.js");
|
|
73
|
+
return await fn(mod);
|
|
74
|
+
} finally {
|
|
75
|
+
vi.doUnmock("./truapi.js");
|
|
76
|
+
vi.resetModules();
|
|
65
77
|
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Tests live inside `describe` so the re-import in `withMockedTruApi`
|
|
81
|
+
// (via `vi.resetModules`) doesn't re-register top-level `test()` calls.
|
|
82
|
+
describe("deriveEntropy", () => {
|
|
83
|
+
test("returns err(HostUnavailableError) when TruAPI is unavailable", async () => {
|
|
84
|
+
await withMockedTruApi(null, async (mod) => {
|
|
85
|
+
const result = await mod.deriveEntropy(new Uint8Array([1, 2, 3]));
|
|
86
|
+
expect(result.ok).toBe(false);
|
|
87
|
+
if (!result.ok) {
|
|
88
|
+
expect(result.error.name).toBe("HostUnavailableError");
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("hex-encodes the context and decodes the entropy bytes", async () => {
|
|
94
|
+
const derive = vi.fn(() => okAsync({ entropy: "0xc0ffee" }));
|
|
95
|
+
await withMockedTruApi({ entropy: { derive } }, async (mod) => {
|
|
96
|
+
const result = await mod.deriveEntropy(new Uint8Array([0xab, 0xcd]));
|
|
97
|
+
expect(derive).toHaveBeenCalledWith({ context: "0xabcd" });
|
|
98
|
+
expect(result.ok).toBe(true);
|
|
99
|
+
if (result.ok) {
|
|
100
|
+
expect(Array.from(result.value)).toEqual([0xc0, 0xff, 0xee]);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
});
|
|
66
104
|
});
|
|
67
105
|
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Typed errors carried on the `err` channel of the host public API's
|
|
5
|
+
* {@link Result} returns.
|
|
6
|
+
*
|
|
7
|
+
* The hierarchy mirrors `@parity/product-sdk-signer`'s error classes
|
|
8
|
+
* (`HostUnavailableError` / `HostRejectedError`), so the two layers share one
|
|
9
|
+
* idiom: branch with `instanceof`, and every error is a real `Error` with a
|
|
10
|
+
* stack trace and `cause`. The structured truapi wire error
|
|
11
|
+
* ({@link HostErrorPayload}) rides along as {@link HostCallFailedError.payload}
|
|
12
|
+
* for callers that want fine-grained tag-level handling.
|
|
13
|
+
*
|
|
14
|
+
* This module also owns {@link HostErrorPayload} (the wire-error shape) and
|
|
15
|
+
* {@link formatHostError} (renders a payload to a message) — co-located with the
|
|
16
|
+
* error classes that consume them so the host error model lives in one place.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
import type { GenericError } from "@parity/truapi";
|
|
21
|
+
|
|
22
|
+
/**
|
|
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.
|
|
37
|
+
*
|
|
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.
|
|
41
|
+
*/
|
|
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";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Extract a human-readable message from a host-side error.
|
|
56
|
+
*
|
|
57
|
+
* Renders the {@link HostErrorPayload} shapes `@parity/truapi` surfaces. Accepts
|
|
58
|
+
* `unknown` because it is also the catch-all formatter for thrown adapter-method
|
|
59
|
+
* `Error` messages, so it falls back to `Error`/string/JSON rendering for
|
|
60
|
+
* anything that isn't a recognized host error payload.
|
|
61
|
+
*
|
|
62
|
+
* Used by {@link HostCallFailedError} to render its message, and by the throwing
|
|
63
|
+
* adapter-method helper `unwrapHostResult`.
|
|
64
|
+
*/
|
|
65
|
+
export function formatHostError(error: unknown): string {
|
|
66
|
+
if (error instanceof Error) return error.message;
|
|
67
|
+
if (typeof error === "string") return error;
|
|
68
|
+
|
|
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;
|
|
77
|
+
}
|
|
78
|
+
// GenericError: { reason }
|
|
79
|
+
return error.reason;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (error != null && typeof error === "object" && "message" in error) {
|
|
83
|
+
const message = (error as { message: unknown }).message;
|
|
84
|
+
if (typeof message === "string") return message;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
return JSON.stringify(error);
|
|
89
|
+
} catch {
|
|
90
|
+
return String(error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Base class for all host errors. Use `instanceof HostError` (or {@link isHostError})
|
|
96
|
+
* to catch any host-related failure.
|
|
97
|
+
*/
|
|
98
|
+
export class HostError extends Error {
|
|
99
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
100
|
+
super(message, options);
|
|
101
|
+
this.name = "HostError";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The host API is not available — the app is running outside a Polkadot host
|
|
107
|
+
* container (no injected TruAPI transport). The dominant case during local
|
|
108
|
+
* development. Branch with `instanceof HostUnavailableError` to surface an
|
|
109
|
+
* "open this app in a Polkadot host" message.
|
|
110
|
+
*/
|
|
111
|
+
export class HostUnavailableError extends HostError {
|
|
112
|
+
constructor(message = "Host API is not available") {
|
|
113
|
+
super(message);
|
|
114
|
+
this.name = "HostUnavailableError";
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A host call reached the container but failed on the `Err` channel. Wraps the
|
|
120
|
+
* structured truapi {@link HostErrorPayload} as {@link payload} (also preserved
|
|
121
|
+
* as `cause`); the message is rendered via {@link formatHostError}.
|
|
122
|
+
*/
|
|
123
|
+
export class HostCallFailedError extends HostError {
|
|
124
|
+
readonly payload: HostErrorPayload;
|
|
125
|
+
|
|
126
|
+
constructor(label: string, payload: HostErrorPayload) {
|
|
127
|
+
super(`${label}: ${formatHostError(payload)}`, { cause: payload });
|
|
128
|
+
this.name = "HostCallFailedError";
|
|
129
|
+
this.payload = payload;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Check whether a value is any {@link HostError}. */
|
|
134
|
+
export function isHostError(error: unknown): error is HostError {
|
|
135
|
+
return error instanceof HostError;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (import.meta.vitest) {
|
|
139
|
+
const { test, expect, describe } = import.meta.vitest;
|
|
140
|
+
|
|
141
|
+
describe("host error classes", () => {
|
|
142
|
+
test("HostError is the base class", () => {
|
|
143
|
+
const e = new HostUnavailableError();
|
|
144
|
+
expect(e).toBeInstanceOf(HostError);
|
|
145
|
+
expect(e).toBeInstanceOf(Error);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("HostUnavailableError default message", () => {
|
|
149
|
+
const e = new HostUnavailableError();
|
|
150
|
+
expect(e.name).toBe("HostUnavailableError");
|
|
151
|
+
expect(e.message).toBe("Host API is not available");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("HostUnavailableError custom message", () => {
|
|
155
|
+
expect(new HostUnavailableError("nope").message).toBe("nope");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("HostCallFailedError renders payload and preserves it", () => {
|
|
159
|
+
const payload = { tag: "PermissionDenied", value: { reason: "user said no" } };
|
|
160
|
+
const e = new HostCallFailedError("requestPermission failed", payload);
|
|
161
|
+
expect(e).toBeInstanceOf(HostError);
|
|
162
|
+
expect(e.payload).toBe(payload);
|
|
163
|
+
expect(e.cause).toBe(payload);
|
|
164
|
+
expect(e.message).toBe("requestPermission failed: PermissionDenied: user said no");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("HostCallFailedError renders a GenericError payload", () => {
|
|
168
|
+
const e = new HostCallFailedError("submit failed", { reason: "timeout" });
|
|
169
|
+
expect(e.message).toBe("submit failed: timeout");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("isHostError narrows host errors only", () => {
|
|
173
|
+
expect(isHostError(new HostUnavailableError())).toBe(true);
|
|
174
|
+
expect(isHostError(new HostCallFailedError("x", { reason: "y" }))).toBe(true);
|
|
175
|
+
expect(isHostError(new Error("plain"))).toBe(false);
|
|
176
|
+
expect(isHostError("string")).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("formatHostError", () => {
|
|
181
|
+
test("renders the TruAPI error payload shapes", () => {
|
|
182
|
+
// GenericError: { reason }
|
|
183
|
+
expect(formatHostError({ reason: "boom" })).toBe("boom");
|
|
184
|
+
// Tagged variant carrying a reason: { tag, value: { reason } }
|
|
185
|
+
expect(formatHostError({ tag: "Unknown", value: { reason: "boom" } })).toBe(
|
|
186
|
+
"Unknown: boom",
|
|
187
|
+
);
|
|
188
|
+
// Unit tagged variant: { tag }
|
|
189
|
+
expect(formatHostError({ tag: "Full" })).toBe("Full");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("falls back for non-host-error input", () => {
|
|
193
|
+
expect(formatHostError(new Error("plain"))).toBe("plain");
|
|
194
|
+
expect(formatHostError("string err")).toBe("string err");
|
|
195
|
+
expect(formatHostError({ message: "loose" })).toBe("loose");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
package/src/features.ts
CHANGED
|
@@ -3,61 +3,62 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Higher-level wrappers for the host's feature-support probe.
|
|
5
5
|
*
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* convenience over the only feature variant the host exposes today (`Chain`).
|
|
6
|
+
* `truApi.system.featureSupported` returns a neverthrow `ResultAsync`;
|
|
7
|
+
* {@link featureSupported} collapses that to a `Result` carrying the host's
|
|
8
|
+
* boolean answer. {@link isChainSupported} is a convenience over the only
|
|
9
|
+
* feature variant the host exposes today (`Chain`).
|
|
11
10
|
*
|
|
12
11
|
* @module
|
|
13
12
|
*/
|
|
14
13
|
|
|
15
14
|
import { createLogger } from "@parity/product-sdk-logger";
|
|
16
15
|
|
|
17
|
-
import {
|
|
16
|
+
import { type HostError, HostUnavailableError } from "./errors.js";
|
|
17
|
+
import { type Result, err } from "./result.js";
|
|
18
|
+
import { getTruApi, type HexString, mapHostResult } from "./truapi.js";
|
|
18
19
|
|
|
19
20
|
const log = createLogger("host:features");
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* A feature the host can be probed for via {@link featureSupported}.
|
|
23
24
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* The only variant today is `Chain`, carrying the chain's `0x`-prefixed genesis
|
|
26
|
+
* hash. This is a flattened form of truapi's `HostFeatureSupportedRequest`,
|
|
27
|
+
* which nests the hash as `{ tag: "Chain"; value: { genesisHash } }` — we
|
|
28
|
+
* inline `value` as the `HexString` for ergonomics and re-nest it at the call
|
|
29
|
+
* site. New variants surface here as a widening of the union.
|
|
28
30
|
*/
|
|
29
31
|
export type Feature = { tag: "Chain"; value: HexString };
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
34
|
* Probe the host for support of a specific feature.
|
|
33
35
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
+
* Calls `truApi.system.featureSupported`, unwraps the response, and returns the
|
|
37
|
+
* host's boolean answer.
|
|
36
38
|
*
|
|
37
39
|
* @param feature - The feature to probe for.
|
|
38
|
-
* @returns `true` if the host supports the feature, `false` otherwise
|
|
39
|
-
*
|
|
40
|
+
* @returns `ok(true)` if the host supports the feature, `ok(false)` otherwise,
|
|
41
|
+
* or `err(HostUnavailableError | HostCallFailedError)`.
|
|
40
42
|
*
|
|
41
43
|
* @example
|
|
42
44
|
* ```ts
|
|
43
45
|
* import { featureSupported } from "@parity/product-sdk-host";
|
|
44
46
|
*
|
|
45
|
-
* const
|
|
47
|
+
* const r = await featureSupported({ tag: "Chain", value: genesisHash });
|
|
48
|
+
* if (r.ok && r.value) { ... }
|
|
46
49
|
* ```
|
|
47
50
|
*/
|
|
48
|
-
export async function featureSupported(feature: Feature): Promise<boolean
|
|
51
|
+
export async function featureSupported(feature: Feature): Promise<Result<boolean, HostError>> {
|
|
49
52
|
const truApi = await getTruApi();
|
|
50
53
|
if (!truApi) {
|
|
51
|
-
|
|
54
|
+
return err(new HostUnavailableError("featureSupported: TruAPI unavailable"));
|
|
52
55
|
}
|
|
53
56
|
log.debug("featureSupported", { tag: feature.tag });
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
(
|
|
58
|
-
|
|
59
|
-
throw new Error(`featureSupported failed: ${formatHostError(err)}`, { cause: err });
|
|
60
|
-
},
|
|
58
|
+
return mapHostResult(
|
|
59
|
+
truApi.system.featureSupported({ tag: feature.tag, value: { genesisHash: feature.value } }),
|
|
60
|
+
(response) => response.supported,
|
|
61
|
+
"featureSupported failed",
|
|
61
62
|
);
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -66,27 +67,30 @@ export async function featureSupported(feature: Feature): Promise<boolean> {
|
|
|
66
67
|
* host? Wraps {@link featureSupported} for the `Chain` feature variant.
|
|
67
68
|
*
|
|
68
69
|
* @param genesisHash - The chain's `0x`-prefixed genesis hash.
|
|
69
|
-
* @returns `true` if the host supports the chain, `false` otherwise
|
|
70
|
-
*
|
|
70
|
+
* @returns `ok(true)` if the host supports the chain, `ok(false)` otherwise, or
|
|
71
|
+
* `err(HostUnavailableError | HostCallFailedError)`.
|
|
71
72
|
*
|
|
72
73
|
* @example
|
|
73
74
|
* ```ts
|
|
74
75
|
* import { isChainSupported } from "@parity/product-sdk-host";
|
|
75
76
|
*
|
|
76
|
-
*
|
|
77
|
+
* const r = await isChainSupported(genesisHash);
|
|
78
|
+
* if (!r.ok || !r.value) {
|
|
77
79
|
* tellUserChainUnavailable();
|
|
78
80
|
* }
|
|
79
81
|
* ```
|
|
80
82
|
*/
|
|
81
|
-
export async function isChainSupported(
|
|
82
|
-
|
|
83
|
+
export async function isChainSupported(
|
|
84
|
+
genesisHash: HexString,
|
|
85
|
+
): Promise<Result<boolean, HostError>> {
|
|
86
|
+
return featureSupported({ tag: "Chain", value: genesisHash });
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
if (import.meta.vitest) {
|
|
86
90
|
const { test, expect, describe, vi } = import.meta.vitest;
|
|
87
91
|
|
|
88
92
|
async function withMockedTruApi<T>(
|
|
89
|
-
bridge: { featureSupported?: (req: unknown) => unknown } | null,
|
|
93
|
+
bridge: { system?: { featureSupported?: (req: unknown) => unknown } } | null,
|
|
90
94
|
fn: (mod: typeof import("./features.js")) => Promise<T>,
|
|
91
95
|
): Promise<T> {
|
|
92
96
|
vi.resetModules();
|
|
@@ -95,7 +99,6 @@ if (import.meta.vitest) {
|
|
|
95
99
|
return {
|
|
96
100
|
...original,
|
|
97
101
|
getTruApi: async () => bridge,
|
|
98
|
-
enumValue: (version: string, value: unknown) => ({ tag: version, value }),
|
|
99
102
|
};
|
|
100
103
|
});
|
|
101
104
|
try {
|
|
@@ -107,45 +110,53 @@ if (import.meta.vitest) {
|
|
|
107
110
|
}
|
|
108
111
|
}
|
|
109
112
|
|
|
110
|
-
const okBridge = (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
const okBridge = (supported: boolean) => ({
|
|
114
|
+
system: {
|
|
115
|
+
featureSupported: vi.fn().mockReturnValue({
|
|
116
|
+
match: async (onOk: (v: unknown) => unknown) => onOk({ supported }),
|
|
117
|
+
}),
|
|
118
|
+
},
|
|
114
119
|
});
|
|
115
120
|
|
|
116
121
|
describe("featureSupported", () => {
|
|
117
|
-
test("
|
|
122
|
+
test("returns err(HostUnavailableError) when TruAPI is unavailable", async () => {
|
|
118
123
|
await withMockedTruApi(null, async (mod) => {
|
|
119
|
-
await
|
|
120
|
-
|
|
121
|
-
)
|
|
124
|
+
const result = await mod.featureSupported({ tag: "Chain", value: "0x00" });
|
|
125
|
+
expect(result.ok).toBe(false);
|
|
126
|
+
if (!result.ok) {
|
|
127
|
+
expect(result.error.name).toBe("HostUnavailableError");
|
|
128
|
+
}
|
|
122
129
|
});
|
|
123
130
|
});
|
|
124
131
|
|
|
125
|
-
test("
|
|
132
|
+
test("returns ok with the boolean outcome", async () => {
|
|
126
133
|
await withMockedTruApi(okBridge(true), async (mod) => {
|
|
127
|
-
expect(await mod.featureSupported({ tag: "Chain", value: "0x00" })).
|
|
134
|
+
expect(await mod.featureSupported({ tag: "Chain", value: "0x00" })).toEqual({
|
|
135
|
+
ok: true,
|
|
136
|
+
value: true,
|
|
137
|
+
});
|
|
128
138
|
});
|
|
129
139
|
});
|
|
130
140
|
|
|
131
|
-
test("wraps host errors with a diagnostic message", async () => {
|
|
141
|
+
test("wraps host errors in err(HostCallFailedError) with a diagnostic message", async () => {
|
|
132
142
|
await withMockedTruApi(
|
|
133
143
|
{
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
onErr({
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}),
|
|
143
|
-
}),
|
|
144
|
+
system: {
|
|
145
|
+
featureSupported: vi.fn().mockReturnValue({
|
|
146
|
+
match: async (
|
|
147
|
+
_onOk: (v: unknown) => unknown,
|
|
148
|
+
onErr: (e: unknown) => unknown,
|
|
149
|
+
) => onErr({ reason: "boom" }),
|
|
150
|
+
}),
|
|
151
|
+
},
|
|
144
152
|
},
|
|
145
153
|
async (mod) => {
|
|
146
|
-
await
|
|
147
|
-
|
|
148
|
-
)
|
|
154
|
+
const result = await mod.featureSupported({ tag: "Chain", value: "0x00" });
|
|
155
|
+
expect(result.ok).toBe(false);
|
|
156
|
+
if (!result.ok) {
|
|
157
|
+
expect(result.error.name).toBe("HostCallFailedError");
|
|
158
|
+
expect(result.error.message).toMatch(/featureSupported failed: boom/);
|
|
159
|
+
}
|
|
149
160
|
},
|
|
150
161
|
);
|
|
151
162
|
});
|
|
@@ -154,7 +165,7 @@ if (import.meta.vitest) {
|
|
|
154
165
|
describe("isChainSupported", () => {
|
|
155
166
|
test("delegates to featureSupported with the Chain variant", async () => {
|
|
156
167
|
await withMockedTruApi(okBridge(false), async (mod) => {
|
|
157
|
-
expect(await mod.isChainSupported("0x1234")).
|
|
168
|
+
expect(await mod.isChainSupported("0x1234")).toEqual({ ok: true, value: false });
|
|
158
169
|
});
|
|
159
170
|
});
|
|
160
171
|
});
|
package/src/index.ts
CHANGED
|
@@ -32,22 +32,14 @@ export type {
|
|
|
32
32
|
} from "./types.js";
|
|
33
33
|
export { BULLETIN_RPCS, DEFAULT_BULLETIN_ENDPOINT } from "./chains.js";
|
|
34
34
|
|
|
35
|
-
// TruAPI -
|
|
35
|
+
// TruAPI - @parity/truapi client accessor + convenience wrappers.
|
|
36
36
|
export {
|
|
37
37
|
getTruApi,
|
|
38
38
|
getPreimageManager,
|
|
39
39
|
createHostPreimageManager,
|
|
40
|
-
getAccountsProvider,
|
|
41
40
|
requestResourceAllocation,
|
|
42
41
|
createProofAuthorized,
|
|
43
|
-
|
|
44
|
-
// Helpers from @novasamatech/host-api
|
|
45
|
-
enumValue,
|
|
46
|
-
isEnumVariant,
|
|
47
|
-
assertEnumVariant,
|
|
48
|
-
unwrapResultOrThrow,
|
|
49
|
-
resultOk,
|
|
50
|
-
resultErr,
|
|
42
|
+
// Hex helpers
|
|
51
43
|
toHex,
|
|
52
44
|
fromHex,
|
|
53
45
|
} from "./truapi.js";
|
|
@@ -55,19 +47,34 @@ export type {
|
|
|
55
47
|
TruApi,
|
|
56
48
|
HexString,
|
|
57
49
|
PreimageManager,
|
|
58
|
-
AccountsProvider,
|
|
59
|
-
HostAccount,
|
|
60
|
-
ProductAccount,
|
|
61
|
-
ContextualAlias,
|
|
62
50
|
ResultAsync,
|
|
63
51
|
AllocatableResource,
|
|
64
|
-
AllocatableResourceTag,
|
|
65
52
|
AllocationOutcome,
|
|
66
|
-
AllocationOutcomeTag,
|
|
67
53
|
RemotePermission,
|
|
68
|
-
RemotePermissionTag,
|
|
69
54
|
} from "./truapi.js";
|
|
70
55
|
|
|
56
|
+
// Result type + typed host errors (the throw→Result boundary)
|
|
57
|
+
export { ok, err } from "./result.js";
|
|
58
|
+
export type { Result } from "./result.js";
|
|
59
|
+
export {
|
|
60
|
+
HostError,
|
|
61
|
+
HostUnavailableError,
|
|
62
|
+
HostCallFailedError,
|
|
63
|
+
isHostError,
|
|
64
|
+
formatHostError,
|
|
65
|
+
} from "./errors.js";
|
|
66
|
+
export type { HostErrorPayload } from "./errors.js";
|
|
67
|
+
|
|
68
|
+
// Accounts — host wallet accounts, product accounts, Ring VRF, and signers.
|
|
69
|
+
export { getAccountsProvider } from "./accounts.js";
|
|
70
|
+
export type {
|
|
71
|
+
AccountsProvider,
|
|
72
|
+
HostAccount,
|
|
73
|
+
ProductAccount,
|
|
74
|
+
ContextualAlias,
|
|
75
|
+
RingLocation,
|
|
76
|
+
} from "./accounts.js";
|
|
77
|
+
|
|
71
78
|
// Higher-level permission wrappers
|
|
72
79
|
export { requestPermission, requestDevicePermission } from "./permissions.js";
|
|
73
80
|
export type { DevicePermissionKind, RemotePermissionItem } from "./permissions.js";
|
|
@@ -80,7 +87,7 @@ export type { ThemeMode, ThemeName, ThemeProvider, ThemeVariant } from "./theme.
|
|
|
80
87
|
export { deriveEntropy } from "./entropy.js";
|
|
81
88
|
|
|
82
89
|
// Chat
|
|
83
|
-
export { getChatManager
|
|
90
|
+
export { getChatManager } from "./chat.js";
|
|
84
91
|
export type {
|
|
85
92
|
ChatManager,
|
|
86
93
|
ChatMessageContent,
|
|
@@ -88,20 +95,24 @@ export type {
|
|
|
88
95
|
ChatRoom,
|
|
89
96
|
ChatRoomRegistrationResult,
|
|
90
97
|
ChatBotRegistrationResult,
|
|
91
|
-
ChatCustomMessageRenderer,
|
|
92
|
-
ChatCustomMessageRendererParams,
|
|
93
98
|
} from "./chat.js";
|
|
94
99
|
|
|
95
100
|
// Payments (RFC-0006)
|
|
96
101
|
export { getPaymentManager } from "./payments.js";
|
|
97
|
-
export type { PaymentManager
|
|
102
|
+
export type { PaymentManager } from "./payments.js";
|
|
103
|
+
export type {
|
|
104
|
+
HostPaymentBalanceSubscribeItem,
|
|
105
|
+
HostPaymentStatusSubscribeItem,
|
|
106
|
+
PaymentTopUpSource,
|
|
107
|
+
} from "@parity/truapi";
|
|
98
108
|
|
|
99
109
|
// Notifications
|
|
100
|
-
export { getNotificationManager
|
|
110
|
+
export { getNotificationManager } from "./notifications.js";
|
|
101
111
|
export type {
|
|
102
112
|
NotificationManager,
|
|
103
113
|
NotificationId,
|
|
104
114
|
PushNotificationInput,
|
|
115
|
+
PushNotificationError,
|
|
105
116
|
} from "./notifications.js";
|
|
106
117
|
|
|
107
118
|
// Deep-link navigation
|