@parity/product-sdk-host 0.10.3 → 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.
- package/dist/index.d.ts +227 -2
- package/dist/index.js +104 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/chain-spec.ts +230 -0
- package/src/chain-transaction.ts +212 -0
- package/src/features.ts +161 -0
- package/src/index.ts +14 -0
- package/src/navigation.ts +127 -0
|
@@ -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
|
+
}
|
package/src/features.ts
ADDED
|
@@ -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";
|