@parity/product-sdk-host 0.10.2 → 0.11.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,230 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Higher-level wrapper for the host's chain-spec lookups.
5
+ *
6
+ * The host exposes three separate chain-spec calls — `chainSpecGenesisHash`,
7
+ * `chainSpecChainName`, and `chainSpecProperties` — each reachable via
8
+ * {@link getTruApi} but each requiring its own `enumValue("v1", ...)` wrap
9
+ * and neverthrow `ResultAsync` unwrap. {@link getChainSpec} fetches all three
10
+ * in one call and returns a single struct so callers read whichever field
11
+ * they need, matching the JSON-RPC `chainSpec_v1_*` family they mirror.
12
+ *
13
+ * @module
14
+ */
15
+
16
+ import { createLogger } from "@parity/product-sdk-logger";
17
+
18
+ import { enumValue, formatHostError, getTruApi, type HexString } from "./truapi.js";
19
+
20
+ const log = createLogger("host:chain-spec");
21
+
22
+ /**
23
+ * Chain SS58/token properties as reported by the host's
24
+ * `chainSpecProperties` call.
25
+ *
26
+ * The host returns this as a JSON string (mirroring the substrate
27
+ * `chainSpec_v1_properties` JSON-RPC, whose payload is an open-ended object).
28
+ * {@link getChainSpec} parses it into {@link properties} and also surfaces the
29
+ * untouched JSON as {@link propertiesRaw}. The well-known substrate fields are
30
+ * typed for convenience; the index signature keeps any chain-specific extras
31
+ * reachable without `any` at the call site.
32
+ */
33
+ export interface ChainProperties {
34
+ /** Address prefix used for SS58 encoding (e.g. `0` for Polkadot). */
35
+ ss58Format?: number;
36
+ /** Decimal places of the chain's native token(s). */
37
+ tokenDecimals?: number | number[];
38
+ /** Ticker symbol(s) of the chain's native token(s). */
39
+ tokenSymbol?: string | string[];
40
+ /** Chain-specific extras passed through verbatim from the JSON payload. */
41
+ [key: string]: unknown;
42
+ }
43
+
44
+ /**
45
+ * Combined chain-spec view returned by {@link getChainSpec}.
46
+ */
47
+ export interface ChainSpec {
48
+ /** The chain's `0x`-prefixed genesis hash, as reported by the host. */
49
+ genesisHash: HexString;
50
+ /** Human-readable chain name (e.g. `"Polkadot"`). */
51
+ name: string;
52
+ /**
53
+ * Parsed chain properties, or `null` if the host's JSON payload couldn't
54
+ * be parsed. Inspect {@link propertiesRaw} for the original string.
55
+ */
56
+ properties: ChainProperties | null;
57
+ /** The untouched JSON string the host returned for properties. */
58
+ propertiesRaw: string;
59
+ }
60
+
61
+ /**
62
+ * Fetch a chain's full spec (genesis hash, name, and properties) from the host
63
+ * in one call.
64
+ *
65
+ * Issues the three underlying `chainSpec*` requests concurrently, unwraps each
66
+ * `v1` envelope, and parses the properties JSON. Note the `genesisHash` in the
67
+ * result is the value the host echoes back from `chainSpecGenesisHash` for the
68
+ * looked-up chain — pass the chain's known genesis hash as the lookup key.
69
+ *
70
+ * @param genesisHash - The `0x`-prefixed genesis hash identifying the chain.
71
+ * @returns The combined {@link ChainSpec}, or `null` if the host is
72
+ * unavailable (running outside a container).
73
+ * @throws If any of the underlying host calls fail (`GenericError`).
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * import { getChainSpec } from "@parity/product-sdk-host";
78
+ *
79
+ * const spec = await getChainSpec(genesisHash);
80
+ * if (spec) {
81
+ * console.log(spec.name, spec.properties?.tokenSymbol);
82
+ * }
83
+ * ```
84
+ */
85
+ export async function getChainSpec(genesisHash: HexString): Promise<ChainSpec | null> {
86
+ const truApi = await getTruApi();
87
+ if (!truApi) {
88
+ log.debug("getChainSpec: TruAPI unavailable");
89
+ return null;
90
+ }
91
+ log.debug("getChainSpec", { genesisHash });
92
+
93
+ // `.match()` because the host returns neverthrow ResultAsync values, not Promises.
94
+ const [resolvedGenesisHash, name, propertiesRaw] = await Promise.all([
95
+ truApi.chainSpecGenesisHash(enumValue("v1", genesisHash)).match(
96
+ (envelope: { tag: "v1"; value: HexString }) => envelope.value,
97
+ (err: unknown) => {
98
+ throw new Error(`getChainSpec (genesisHash) failed: ${formatHostError(err)}`, {
99
+ cause: err,
100
+ });
101
+ },
102
+ ),
103
+ truApi.chainSpecChainName(enumValue("v1", genesisHash)).match(
104
+ (envelope: { tag: "v1"; value: string }) => envelope.value,
105
+ (err: unknown) => {
106
+ throw new Error(`getChainSpec (chainName) failed: ${formatHostError(err)}`, {
107
+ cause: err,
108
+ });
109
+ },
110
+ ),
111
+ truApi.chainSpecProperties(enumValue("v1", genesisHash)).match(
112
+ (envelope: { tag: "v1"; value: string }) => envelope.value,
113
+ (err: unknown) => {
114
+ throw new Error(`getChainSpec (properties) failed: ${formatHostError(err)}`, {
115
+ cause: err,
116
+ });
117
+ },
118
+ ),
119
+ ]);
120
+
121
+ let properties: ChainProperties | null;
122
+ try {
123
+ properties = JSON.parse(propertiesRaw) as ChainProperties;
124
+ } catch (err) {
125
+ log.debug("getChainSpec: properties JSON parse failed", err);
126
+ properties = null;
127
+ }
128
+
129
+ return { genesisHash: resolvedGenesisHash, name, properties, propertiesRaw };
130
+ }
131
+
132
+ if (import.meta.vitest) {
133
+ const { test, expect, describe, vi } = import.meta.vitest;
134
+
135
+ async function withMockedTruApi<T>(
136
+ bridge: {
137
+ chainSpecGenesisHash?: (req: unknown) => unknown;
138
+ chainSpecChainName?: (req: unknown) => unknown;
139
+ chainSpecProperties?: (req: unknown) => unknown;
140
+ } | null,
141
+ fn: (mod: typeof import("./chain-spec.js")) => Promise<T>,
142
+ ): Promise<T> {
143
+ vi.resetModules();
144
+ vi.doMock("./truapi.js", async (importOriginal) => {
145
+ const original = await importOriginal<typeof import("./truapi.js")>();
146
+ return {
147
+ ...original,
148
+ getTruApi: async () => bridge,
149
+ enumValue: (version: string, value: unknown) => ({ tag: version, value }),
150
+ };
151
+ });
152
+ try {
153
+ const mod = await import("./chain-spec.js");
154
+ return await fn(mod);
155
+ } finally {
156
+ vi.doUnmock("./truapi.js");
157
+ vi.resetModules();
158
+ }
159
+ }
160
+
161
+ const ok = (value: unknown) => ({
162
+ match: async (onOk: (v: unknown) => unknown) => onOk({ tag: "v1", value }),
163
+ });
164
+
165
+ describe("getChainSpec", () => {
166
+ test("returns null when TruAPI is unavailable", async () => {
167
+ await withMockedTruApi(null, async (mod) => {
168
+ expect(await mod.getChainSpec("0x00")).toBeNull();
169
+ });
170
+ });
171
+
172
+ test("combines the three calls and parses properties JSON", async () => {
173
+ await withMockedTruApi(
174
+ {
175
+ chainSpecGenesisHash: vi.fn().mockReturnValue(ok("0xabcd")),
176
+ chainSpecChainName: vi.fn().mockReturnValue(ok("Polkadot")),
177
+ chainSpecProperties: vi
178
+ .fn()
179
+ .mockReturnValue(
180
+ ok('{"ss58Format":0,"tokenDecimals":10,"tokenSymbol":"DOT"}'),
181
+ ),
182
+ },
183
+ async (mod) => {
184
+ const spec = await mod.getChainSpec("0xabcd");
185
+ expect(spec).toEqual({
186
+ genesisHash: "0xabcd",
187
+ name: "Polkadot",
188
+ properties: { ss58Format: 0, tokenDecimals: 10, tokenSymbol: "DOT" },
189
+ propertiesRaw: '{"ss58Format":0,"tokenDecimals":10,"tokenSymbol":"DOT"}',
190
+ });
191
+ },
192
+ );
193
+ });
194
+
195
+ test("leaves properties null when the JSON is malformed", async () => {
196
+ await withMockedTruApi(
197
+ {
198
+ chainSpecGenesisHash: vi.fn().mockReturnValue(ok("0xabcd")),
199
+ chainSpecChainName: vi.fn().mockReturnValue(ok("Polkadot")),
200
+ chainSpecProperties: vi.fn().mockReturnValue(ok("not json")),
201
+ },
202
+ async (mod) => {
203
+ const spec = await mod.getChainSpec("0xabcd");
204
+ expect(spec?.properties).toBeNull();
205
+ expect(spec?.propertiesRaw).toBe("not json");
206
+ },
207
+ );
208
+ });
209
+
210
+ test("wraps host errors with a diagnostic message", async () => {
211
+ await withMockedTruApi(
212
+ {
213
+ chainSpecGenesisHash: vi.fn().mockReturnValue({
214
+ match: async (
215
+ _onOk: (v: unknown) => unknown,
216
+ onErr: (e: unknown) => unknown,
217
+ ) => onErr({ tag: "v1", value: { name: "GenericError", message: "boom" } }),
218
+ }),
219
+ chainSpecChainName: vi.fn().mockReturnValue(ok("Polkadot")),
220
+ chainSpecProperties: vi.fn().mockReturnValue(ok("{}")),
221
+ },
222
+ async (mod) => {
223
+ await expect(mod.getChainSpec("0xabcd")).rejects.toThrow(
224
+ /getChainSpec \(genesisHash\) failed: GenericError: boom/,
225
+ );
226
+ },
227
+ );
228
+ });
229
+ });
230
+ }
@@ -0,0 +1,212 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Higher-level wrappers for the host's transaction broadcast lifecycle.
5
+ *
6
+ * `hostApi.chainTransactionBroadcast` / `hostApi.chainTransactionStop` are
7
+ * reachable via {@link getTruApi}, but consumers have to build the versioned
8
+ * envelope (`enumValue("v1", ...)`) and unwrap the neverthrow `ResultAsync`
9
+ * themselves. {@link broadcastTransaction} and {@link stopTransaction}
10
+ * collapse that to throw-on-error Promises, mirroring the JSON-RPC
11
+ * `transaction_v1_broadcast` / `transaction_v1_stop` pair they wrap.
12
+ *
13
+ * @module
14
+ */
15
+
16
+ import { createLogger } from "@parity/product-sdk-logger";
17
+
18
+ import { enumValue, formatHostError, getTruApi, type HexString } from "./truapi.js";
19
+
20
+ const log = createLogger("host:chain-transaction");
21
+
22
+ /**
23
+ * Broadcast a signed transaction to the network via the host.
24
+ *
25
+ * Builds the `v1` envelope, calls `hostApi.chainTransactionBroadcast`, and
26
+ * unwraps the response. The host keeps re-broadcasting until the transaction
27
+ * is finalized/dropped or {@link stopTransaction} is called with the returned
28
+ * operation id.
29
+ *
30
+ * @param genesisHash - The `0x`-prefixed genesis hash of the target chain.
31
+ * @param transaction - The `0x`-prefixed SCALE-encoded signed transaction.
32
+ * @returns The operation id to pass to {@link stopTransaction}, or `null` if
33
+ * the host accepted the broadcast without issuing one.
34
+ * @throws If the host is unavailable or the broadcast fails (`GenericError`).
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * import { broadcastTransaction, stopTransaction } from "@parity/product-sdk-host";
39
+ *
40
+ * const operationId = await broadcastTransaction(genesisHash, signedTx);
41
+ * // later, to stop re-broadcasting:
42
+ * if (operationId) await stopTransaction(genesisHash, operationId);
43
+ * ```
44
+ */
45
+ export async function broadcastTransaction(
46
+ genesisHash: HexString,
47
+ transaction: HexString,
48
+ ): Promise<string | null> {
49
+ const truApi = await getTruApi();
50
+ if (!truApi) {
51
+ throw new Error("broadcastTransaction: TruAPI unavailable");
52
+ }
53
+ log.debug("broadcastTransaction", { genesisHash });
54
+
55
+ // `.match()` because the host returns a neverthrow ResultAsync, not a Promise.
56
+ return await truApi
57
+ .chainTransactionBroadcast(enumValue("v1", { genesisHash, transaction }))
58
+ .match(
59
+ (envelope: { tag: "v1"; value: string | null }) => envelope.value,
60
+ (err: unknown) => {
61
+ throw new Error(`broadcastTransaction failed: ${formatHostError(err)}`, {
62
+ cause: err,
63
+ });
64
+ },
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Stop an in-flight broadcast started by {@link broadcastTransaction}.
70
+ *
71
+ * Builds the `v1` envelope, calls `hostApi.chainTransactionStop`, and unwraps
72
+ * the response.
73
+ *
74
+ * @param genesisHash - The `0x`-prefixed genesis hash of the target chain.
75
+ * @param operationId - The operation id returned by
76
+ * {@link broadcastTransaction}.
77
+ * @throws If the host is unavailable or the stop fails (`GenericError`).
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * await stopTransaction(genesisHash, operationId);
82
+ * ```
83
+ */
84
+ export async function stopTransaction(genesisHash: HexString, operationId: string): Promise<void> {
85
+ const truApi = await getTruApi();
86
+ if (!truApi) {
87
+ throw new Error("stopTransaction: TruAPI unavailable");
88
+ }
89
+ log.debug("stopTransaction", { genesisHash, operationId });
90
+
91
+ // `.match()` because the host returns a neverthrow ResultAsync, not a Promise.
92
+ await truApi.chainTransactionStop(enumValue("v1", { genesisHash, operationId })).match(
93
+ (_envelope: { tag: "v1"; value: undefined }) => undefined,
94
+ (err: unknown) => {
95
+ throw new Error(`stopTransaction failed: ${formatHostError(err)}`, { cause: err });
96
+ },
97
+ );
98
+ }
99
+
100
+ if (import.meta.vitest) {
101
+ const { test, expect, describe, vi } = import.meta.vitest;
102
+
103
+ async function withMockedTruApi<T>(
104
+ bridge: {
105
+ chainTransactionBroadcast?: (req: unknown) => unknown;
106
+ chainTransactionStop?: (req: unknown) => unknown;
107
+ } | null,
108
+ fn: (mod: typeof import("./chain-transaction.js")) => Promise<T>,
109
+ ): Promise<T> {
110
+ vi.resetModules();
111
+ vi.doMock("./truapi.js", async (importOriginal) => {
112
+ const original = await importOriginal<typeof import("./truapi.js")>();
113
+ return {
114
+ ...original,
115
+ getTruApi: async () => bridge,
116
+ enumValue: (version: string, value: unknown) => ({ tag: version, value }),
117
+ };
118
+ });
119
+ try {
120
+ const mod = await import("./chain-transaction.js");
121
+ return await fn(mod);
122
+ } finally {
123
+ vi.doUnmock("./truapi.js");
124
+ vi.resetModules();
125
+ }
126
+ }
127
+
128
+ const ok = (value: unknown) => ({
129
+ match: async (onOk: (v: unknown) => unknown) => onOk({ tag: "v1", value }),
130
+ });
131
+ const errResult = (name: string, message: string) => ({
132
+ match: async (_onOk: (v: unknown) => unknown, onErr: (e: unknown) => unknown) =>
133
+ onErr({ tag: "v1", value: { name, message } }),
134
+ });
135
+
136
+ describe("broadcastTransaction", () => {
137
+ test("throws when TruAPI is unavailable", async () => {
138
+ await withMockedTruApi(null, async (mod) => {
139
+ await expect(mod.broadcastTransaction("0x00", "0x01")).rejects.toThrow(
140
+ /TruAPI unavailable/,
141
+ );
142
+ });
143
+ });
144
+
145
+ test("unwraps the operation id", async () => {
146
+ await withMockedTruApi(
147
+ { chainTransactionBroadcast: vi.fn().mockReturnValue(ok("op-1")) },
148
+ async (mod) => {
149
+ expect(await mod.broadcastTransaction("0x00", "0x01")).toBe("op-1");
150
+ },
151
+ );
152
+ });
153
+
154
+ test("passes through a null operation id", async () => {
155
+ await withMockedTruApi(
156
+ { chainTransactionBroadcast: vi.fn().mockReturnValue(ok(null)) },
157
+ async (mod) => {
158
+ expect(await mod.broadcastTransaction("0x00", "0x01")).toBeNull();
159
+ },
160
+ );
161
+ });
162
+
163
+ test("wraps host errors with a diagnostic message", async () => {
164
+ await withMockedTruApi(
165
+ {
166
+ chainTransactionBroadcast: vi
167
+ .fn()
168
+ .mockReturnValue(errResult("GenericError", "boom")),
169
+ },
170
+ async (mod) => {
171
+ await expect(mod.broadcastTransaction("0x00", "0x01")).rejects.toThrow(
172
+ /broadcastTransaction failed: GenericError: boom/,
173
+ );
174
+ },
175
+ );
176
+ });
177
+ });
178
+
179
+ describe("stopTransaction", () => {
180
+ test("throws when TruAPI is unavailable", async () => {
181
+ await withMockedTruApi(null, async (mod) => {
182
+ await expect(mod.stopTransaction("0x00", "op-1")).rejects.toThrow(
183
+ /TruAPI unavailable/,
184
+ );
185
+ });
186
+ });
187
+
188
+ test("resolves on the v1 success envelope", async () => {
189
+ await withMockedTruApi(
190
+ { chainTransactionStop: vi.fn().mockReturnValue(ok(undefined)) },
191
+ async (mod) => {
192
+ await expect(mod.stopTransaction("0x00", "op-1")).resolves.toBeUndefined();
193
+ },
194
+ );
195
+ });
196
+
197
+ test("wraps host errors with a diagnostic message", async () => {
198
+ await withMockedTruApi(
199
+ {
200
+ chainTransactionStop: vi
201
+ .fn()
202
+ .mockReturnValue(errResult("GenericError", "boom")),
203
+ },
204
+ async (mod) => {
205
+ await expect(mod.stopTransaction("0x00", "op-1")).rejects.toThrow(
206
+ /stopTransaction failed: GenericError: boom/,
207
+ );
208
+ },
209
+ );
210
+ });
211
+ });
212
+ }
@@ -0,0 +1,161 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Higher-level wrappers for the host's feature-support probe.
5
+ *
6
+ * `hostApi.featureSupported` is reachable via {@link getTruApi}, but consumers
7
+ * have to wrap the feature in the versioned envelope (`enumValue("v1", ...)`)
8
+ * and unwrap the neverthrow `ResultAsync` themselves. {@link featureSupported}
9
+ * collapses that to a throw-on-error Promise; {@link isChainSupported} is a
10
+ * convenience over the only feature variant the host exposes today (`Chain`).
11
+ *
12
+ * @module
13
+ */
14
+
15
+ import { createLogger } from "@parity/product-sdk-logger";
16
+
17
+ import { enumValue, formatHostError, getTruApi, type HexString } from "./truapi.js";
18
+
19
+ const log = createLogger("host:features");
20
+
21
+ /**
22
+ * A feature the host can be probed for via {@link featureSupported}.
23
+ *
24
+ * As of `host-api` v0.8 the only variant is `Chain`, carrying the chain's
25
+ * `0x`-prefixed genesis hash. Modeled locally (rather than derived from an
26
+ * upstream codec) because the protocol exposes the feature only inline; new
27
+ * variants surface here as a widening of the union.
28
+ */
29
+ export type Feature = { tag: "Chain"; value: HexString };
30
+
31
+ /**
32
+ * Probe the host for support of a specific feature.
33
+ *
34
+ * Builds the `v1` envelope, calls `hostApi.featureSupported`, unwraps the
35
+ * response, and returns the host's boolean answer.
36
+ *
37
+ * @param feature - The feature to probe for.
38
+ * @returns `true` if the host supports the feature, `false` otherwise.
39
+ * @throws If the host is unavailable or the probe fails (`GenericError`).
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * import { featureSupported } from "@parity/product-sdk-host";
44
+ *
45
+ * const ok = await featureSupported({ tag: "Chain", value: genesisHash });
46
+ * ```
47
+ */
48
+ export async function featureSupported(feature: Feature): Promise<boolean> {
49
+ const truApi = await getTruApi();
50
+ if (!truApi) {
51
+ throw new Error("featureSupported: TruAPI unavailable");
52
+ }
53
+ log.debug("featureSupported", { tag: feature.tag });
54
+
55
+ // `.match()` because the host returns a neverthrow ResultAsync, not a Promise.
56
+ return await truApi.featureSupported(enumValue("v1", feature)).match(
57
+ (envelope: { tag: "v1"; value: boolean }) => envelope.value,
58
+ (err: unknown) => {
59
+ throw new Error(`featureSupported failed: ${formatHostError(err)}`, { cause: err });
60
+ },
61
+ );
62
+ }
63
+
64
+ /**
65
+ * Convenience probe: is the chain with the given genesis hash supported by the
66
+ * host? Wraps {@link featureSupported} for the `Chain` feature variant.
67
+ *
68
+ * @param genesisHash - The chain's `0x`-prefixed genesis hash.
69
+ * @returns `true` if the host supports the chain, `false` otherwise.
70
+ * @throws If the host is unavailable or the probe fails.
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * import { isChainSupported } from "@parity/product-sdk-host";
75
+ *
76
+ * if (!(await isChainSupported(genesisHash))) {
77
+ * tellUserChainUnavailable();
78
+ * }
79
+ * ```
80
+ */
81
+ export async function isChainSupported(genesisHash: HexString): Promise<boolean> {
82
+ return await featureSupported({ tag: "Chain", value: genesisHash });
83
+ }
84
+
85
+ if (import.meta.vitest) {
86
+ const { test, expect, describe, vi } = import.meta.vitest;
87
+
88
+ async function withMockedTruApi<T>(
89
+ bridge: { featureSupported?: (req: unknown) => unknown } | null,
90
+ fn: (mod: typeof import("./features.js")) => Promise<T>,
91
+ ): Promise<T> {
92
+ vi.resetModules();
93
+ vi.doMock("./truapi.js", async (importOriginal) => {
94
+ const original = await importOriginal<typeof import("./truapi.js")>();
95
+ return {
96
+ ...original,
97
+ getTruApi: async () => bridge,
98
+ enumValue: (version: string, value: unknown) => ({ tag: version, value }),
99
+ };
100
+ });
101
+ try {
102
+ const mod = await import("./features.js");
103
+ return await fn(mod);
104
+ } finally {
105
+ vi.doUnmock("./truapi.js");
106
+ vi.resetModules();
107
+ }
108
+ }
109
+
110
+ const okBridge = (value: boolean) => ({
111
+ featureSupported: vi.fn().mockReturnValue({
112
+ match: async (onOk: (v: unknown) => unknown) => onOk({ tag: "v1", value }),
113
+ }),
114
+ });
115
+
116
+ describe("featureSupported", () => {
117
+ test("throws when TruAPI is unavailable", async () => {
118
+ await withMockedTruApi(null, async (mod) => {
119
+ await expect(mod.featureSupported({ tag: "Chain", value: "0x00" })).rejects.toThrow(
120
+ /TruAPI unavailable/,
121
+ );
122
+ });
123
+ });
124
+
125
+ test("unwraps the v1 boolean outcome", async () => {
126
+ await withMockedTruApi(okBridge(true), async (mod) => {
127
+ expect(await mod.featureSupported({ tag: "Chain", value: "0x00" })).toBe(true);
128
+ });
129
+ });
130
+
131
+ test("wraps host errors with a diagnostic message", async () => {
132
+ await withMockedTruApi(
133
+ {
134
+ featureSupported: vi.fn().mockReturnValue({
135
+ match: async (
136
+ _onOk: (v: unknown) => unknown,
137
+ onErr: (e: unknown) => unknown,
138
+ ) =>
139
+ onErr({
140
+ tag: "v1",
141
+ value: { name: "GenericError", message: "boom" },
142
+ }),
143
+ }),
144
+ },
145
+ async (mod) => {
146
+ await expect(
147
+ mod.featureSupported({ tag: "Chain", value: "0x00" }),
148
+ ).rejects.toThrow(/featureSupported failed: GenericError: boom/);
149
+ },
150
+ );
151
+ });
152
+ });
153
+
154
+ describe("isChainSupported", () => {
155
+ test("delegates to featureSupported with the Chain variant", async () => {
156
+ await withMockedTruApi(okBridge(false), async (mod) => {
157
+ expect(await mod.isChainSupported("0x1234")).toBe(false);
158
+ });
159
+ });
160
+ });
161
+ }
package/src/index.ts CHANGED
@@ -103,3 +103,17 @@ export type {
103
103
  NotificationId,
104
104
  PushNotificationInput,
105
105
  } from "./notifications.js";
106
+
107
+ // Deep-link navigation
108
+ export { navigateTo } from "./navigation.js";
109
+
110
+ // Feature / chain support probes
111
+ export { featureSupported, isChainSupported } from "./features.js";
112
+ export type { Feature } from "./features.js";
113
+
114
+ // Chain spec lookups
115
+ export { getChainSpec } from "./chain-spec.js";
116
+ export type { ChainSpec, ChainProperties } from "./chain-spec.js";
117
+
118
+ // Transaction broadcast lifecycle
119
+ export { broadcastTransaction, stopTransaction } from "./chain-transaction.js";