@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,241 @@
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
+ * `truApi.chain.broadcastTransaction` / `truApi.chain.stopTransaction` are
7
+ * reachable via {@link getTruApi}, but consumers have to unwrap the neverthrow
8
+ * `ResultAsync` themselves. {@link broadcastTransaction} and
9
+ * {@link stopTransaction} collapse that to `Result`-returning Promises, mirroring
10
+ * the JSON-RPC `transaction_v1_broadcast` / `transaction_v1_stop` pair they
11
+ * wrap.
12
+ *
13
+ * @module
14
+ */
15
+
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, type HexString, mapHostResult } from "./truapi.js";
21
+
22
+ const log = createLogger("host:chain-transaction");
23
+
24
+ /**
25
+ * Broadcast a signed transaction to the network via the host.
26
+ *
27
+ * Calls `truApi.chain.broadcastTransaction` and unwraps the response. The host
28
+ * keeps re-broadcasting until the transaction is finalized/dropped or
29
+ * {@link stopTransaction} is called with the returned operation id.
30
+ *
31
+ * @param genesisHash - The `0x`-prefixed genesis hash of the target chain.
32
+ * @param transaction - The `0x`-prefixed SCALE-encoded signed transaction.
33
+ * @returns `ok` with the operation id to pass to {@link stopTransaction} (or
34
+ * `null` if the host accepted the broadcast without issuing one), or
35
+ * `err(HostUnavailableError | HostCallFailedError)`.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * import { broadcastTransaction, stopTransaction } from "@parity/product-sdk-host";
40
+ *
41
+ * const r = await broadcastTransaction(genesisHash, signedTx);
42
+ * // later, to stop re-broadcasting:
43
+ * if (r.ok && r.value) await stopTransaction(genesisHash, r.value);
44
+ * ```
45
+ */
46
+ export async function broadcastTransaction(
47
+ genesisHash: HexString,
48
+ transaction: HexString,
49
+ ): Promise<Result<string | null, HostError>> {
50
+ const truApi = await getTruApi();
51
+ if (!truApi) {
52
+ return err(new HostUnavailableError("broadcastTransaction: TruAPI unavailable"));
53
+ }
54
+ log.debug("broadcastTransaction", { genesisHash });
55
+
56
+ return mapHostResult(
57
+ truApi.chain.broadcastTransaction({ genesisHash, transaction }),
58
+ (response) => response.operationId ?? null,
59
+ "broadcastTransaction failed",
60
+ );
61
+ }
62
+
63
+ /**
64
+ * Stop an in-flight broadcast started by {@link broadcastTransaction}.
65
+ *
66
+ * Calls `truApi.chain.stopTransaction` and unwraps the response.
67
+ *
68
+ * @param genesisHash - The `0x`-prefixed genesis hash of the target chain.
69
+ * @param operationId - The operation id returned by
70
+ * {@link broadcastTransaction}.
71
+ * @returns `ok` on success, or `err(HostUnavailableError | HostCallFailedError)`.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * await stopTransaction(genesisHash, operationId);
76
+ * ```
77
+ */
78
+ export async function stopTransaction(
79
+ genesisHash: HexString,
80
+ operationId: string,
81
+ ): Promise<Result<void, HostError>> {
82
+ const truApi = await getTruApi();
83
+ if (!truApi) {
84
+ return err(new HostUnavailableError("stopTransaction: TruAPI unavailable"));
85
+ }
86
+ log.debug("stopTransaction", { genesisHash, operationId });
87
+
88
+ return mapHostResult(
89
+ truApi.chain.stopTransaction({ genesisHash, operationId }),
90
+ () => undefined,
91
+ "stopTransaction failed",
92
+ );
93
+ }
94
+
95
+ if (import.meta.vitest) {
96
+ const { test, expect, describe, vi } = import.meta.vitest;
97
+
98
+ async function withMockedTruApi<T>(
99
+ bridge: {
100
+ chain?: {
101
+ broadcastTransaction?: (req: unknown) => unknown;
102
+ stopTransaction?: (req: unknown) => unknown;
103
+ };
104
+ } | null,
105
+ fn: (mod: typeof import("./chain-transaction.js")) => Promise<T>,
106
+ ): Promise<T> {
107
+ vi.resetModules();
108
+ vi.doMock("./truapi.js", async (importOriginal) => {
109
+ const original = await importOriginal<typeof import("./truapi.js")>();
110
+ return {
111
+ ...original,
112
+ getTruApi: async () => bridge,
113
+ };
114
+ });
115
+ try {
116
+ const mod = await import("./chain-transaction.js");
117
+ return await fn(mod);
118
+ } finally {
119
+ vi.doUnmock("./truapi.js");
120
+ vi.resetModules();
121
+ }
122
+ }
123
+
124
+ /** A resolved ResultAsync stub yielding the given response object. */
125
+ const ok = (response: unknown) => ({
126
+ match: async (onOk: (v: unknown) => unknown) => onOk(response),
127
+ });
128
+ /** A rejected ResultAsync stub yielding a truapi `GenericError` (`{ reason }`). */
129
+ const errResult = (reason: string) => ({
130
+ match: async (_onOk: (v: unknown) => unknown, onErr: (e: unknown) => unknown) =>
131
+ onErr({ reason }),
132
+ });
133
+
134
+ describe("broadcastTransaction", () => {
135
+ test("returns err(HostUnavailableError) when TruAPI is unavailable", async () => {
136
+ await withMockedTruApi(null, async (mod) => {
137
+ const result = await mod.broadcastTransaction("0x00", "0x01");
138
+ expect(result.ok).toBe(false);
139
+ if (!result.ok) {
140
+ expect(result.error.name).toBe("HostUnavailableError");
141
+ }
142
+ });
143
+ });
144
+
145
+ test("returns ok with the operation id", async () => {
146
+ await withMockedTruApi(
147
+ {
148
+ chain: {
149
+ broadcastTransaction: vi.fn().mockReturnValue(ok({ operationId: "op-1" })),
150
+ },
151
+ },
152
+ async (mod) => {
153
+ expect(await mod.broadcastTransaction("0x00", "0x01")).toEqual({
154
+ ok: true,
155
+ value: "op-1",
156
+ });
157
+ },
158
+ );
159
+ });
160
+
161
+ test("passes through a missing operation id as ok(null)", async () => {
162
+ await withMockedTruApi(
163
+ {
164
+ chain: {
165
+ broadcastTransaction: vi.fn().mockReturnValue(ok({})),
166
+ },
167
+ },
168
+ async (mod) => {
169
+ expect(await mod.broadcastTransaction("0x00", "0x01")).toEqual({
170
+ ok: true,
171
+ value: null,
172
+ });
173
+ },
174
+ );
175
+ });
176
+
177
+ test("wraps host errors in err(HostCallFailedError) with a diagnostic message", async () => {
178
+ await withMockedTruApi(
179
+ {
180
+ chain: {
181
+ broadcastTransaction: vi.fn().mockReturnValue(errResult("boom")),
182
+ },
183
+ },
184
+ async (mod) => {
185
+ const result = await mod.broadcastTransaction("0x00", "0x01");
186
+ expect(result.ok).toBe(false);
187
+ if (!result.ok) {
188
+ expect(result.error.name).toBe("HostCallFailedError");
189
+ expect(result.error.message).toMatch(/broadcastTransaction failed: boom/);
190
+ }
191
+ },
192
+ );
193
+ });
194
+ });
195
+
196
+ describe("stopTransaction", () => {
197
+ test("returns err(HostUnavailableError) when TruAPI is unavailable", async () => {
198
+ await withMockedTruApi(null, async (mod) => {
199
+ const result = await mod.stopTransaction("0x00", "op-1");
200
+ expect(result.ok).toBe(false);
201
+ if (!result.ok) {
202
+ expect(result.error.name).toBe("HostUnavailableError");
203
+ }
204
+ });
205
+ });
206
+
207
+ test("returns ok on success", async () => {
208
+ await withMockedTruApi(
209
+ {
210
+ chain: {
211
+ stopTransaction: vi.fn().mockReturnValue(ok(undefined)),
212
+ },
213
+ },
214
+ async (mod) => {
215
+ expect(await mod.stopTransaction("0x00", "op-1")).toEqual({
216
+ ok: true,
217
+ value: undefined,
218
+ });
219
+ },
220
+ );
221
+ });
222
+
223
+ test("wraps host errors in err(HostCallFailedError) with a diagnostic message", async () => {
224
+ await withMockedTruApi(
225
+ {
226
+ chain: {
227
+ stopTransaction: vi.fn().mockReturnValue(errResult("boom")),
228
+ },
229
+ },
230
+ async (mod) => {
231
+ const result = await mod.stopTransaction("0x00", "op-1");
232
+ expect(result.ok).toBe(false);
233
+ if (!result.ok) {
234
+ expect(result.error.name).toBe("HostCallFailedError");
235
+ expect(result.error.message).toMatch(/stopTransaction failed: boom/);
236
+ }
237
+ },
238
+ );
239
+ });
240
+ });
241
+ }
package/src/chains.ts ADDED
@@ -0,0 +1,46 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Shared chain network configuration — single source of truth for
5
+ * chain-specific endpoints used by multiple packages.
6
+ */
7
+
8
+ /**
9
+ * Bulletin Chain RPC endpoints per network environment. `paseo` (Paseo Next v2)
10
+ * and `devnet` (public Paseo testnet) are populated today; `polkadot` and
11
+ * `kusama` are reserved for when those Bulletin deployments go live.
12
+ */
13
+ export const BULLETIN_RPCS = {
14
+ paseo: ["wss://paseo-bulletin-next-rpc.polkadot.io"],
15
+ devnet: ["wss://bulletin-paseo.tservices.es:8443"],
16
+ polkadot: [] as string[],
17
+ kusama: [] as string[],
18
+ } as const;
19
+
20
+ /** Default Bulletin Chain endpoint — the first entry under {@link BULLETIN_RPCS}.paseo. */
21
+ export const DEFAULT_BULLETIN_ENDPOINT: string = BULLETIN_RPCS.paseo[0];
22
+
23
+ if (import.meta.vitest) {
24
+ const { describe, test, expect } = import.meta.vitest;
25
+
26
+ describe("chains config", () => {
27
+ test("BULLETIN_RPCS has paseo endpoint", () => {
28
+ expect(BULLETIN_RPCS.paseo.length).toBeGreaterThan(0);
29
+ expect(BULLETIN_RPCS.paseo[0]).toMatch(/^wss:\/\//);
30
+ });
31
+
32
+ test("BULLETIN_RPCS has devnet endpoint", () => {
33
+ expect(BULLETIN_RPCS.devnet.length).toBeGreaterThan(0);
34
+ expect(BULLETIN_RPCS.devnet[0]).toMatch(/^wss:\/\//);
35
+ });
36
+
37
+ test("BULLETIN_RPCS polkadot and kusama are empty until live", () => {
38
+ expect(BULLETIN_RPCS.polkadot).toEqual([]);
39
+ expect(BULLETIN_RPCS.kusama).toEqual([]);
40
+ });
41
+
42
+ test("DEFAULT_BULLETIN_ENDPOINT matches first paseo endpoint", () => {
43
+ expect(DEFAULT_BULLETIN_ENDPOINT).toBe(BULLETIN_RPCS.paseo[0]);
44
+ });
45
+ });
46
+ }
package/src/chat.ts ADDED
@@ -0,0 +1,122 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Wrapper for the host's chat surface, backed by `truApi.chat.*`.
5
+ *
6
+ * `getChatManager()` returns a manager for room/bot registration, message
7
+ * sending, and subscription to the room list and incoming actions.
8
+ *
9
+ * @module
10
+ */
11
+
12
+ import type {
13
+ ChatBotRegistrationStatus,
14
+ ChatRoomRegistrationStatus,
15
+ HostChatActionSubscribeItem,
16
+ HostChatCreateRoomRequest,
17
+ HostChatRegisterBotRequest,
18
+ TrUApiClient,
19
+ } from "@parity/truapi";
20
+
21
+ import { getClient, subscribeWithInterrupt } from "./transport.js";
22
+ import { unwrapHostResult } from "./truapi.js";
23
+ import type { HostSubscription } from "./types.js";
24
+
25
+ /** Chat message payload variants and room metadata. Re-exported from `@parity/truapi`. */
26
+ export type { ChatMessageContent, ChatRoom } from "@parity/truapi";
27
+ import type { ChatMessageContent, ChatRoom } from "@parity/truapi";
28
+
29
+ /** Action received via {@link ChatManager.subscribeAction} (`{ roomId, peer, payload }`). Re-exported from `@parity/truapi`. */
30
+ export type ChatReceivedAction = HostChatActionSubscribeItem;
31
+
32
+ /** Result of registering a chat room (`"New" | "Exists"`). Re-exported from `@parity/truapi`. */
33
+ export type ChatRoomRegistrationResult = ChatRoomRegistrationStatus;
34
+
35
+ /** Result of registering a bot (`"New" | "Exists"`). Re-exported from `@parity/truapi`. */
36
+ export type ChatBotRegistrationResult = ChatBotRegistrationStatus;
37
+
38
+ /**
39
+ * Chat manager handle. Exposes room/bot registration, message sending, and
40
+ * subscription to the room list and incoming actions.
41
+ */
42
+ export interface ChatManager {
43
+ registerRoom(request: HostChatCreateRoomRequest): Promise<ChatRoomRegistrationResult>;
44
+ registerBot(request: HostChatRegisterBotRequest): Promise<ChatBotRegistrationResult>;
45
+ sendMessage(roomId: string, payload: ChatMessageContent): Promise<{ messageId: string }>;
46
+ subscribeChatList(callback: (rooms: ChatRoom[]) => void): HostSubscription;
47
+ subscribeAction(callback: (action: ChatReceivedAction) => void): HostSubscription;
48
+ }
49
+
50
+ /** Build a {@link ChatManager} over a TruAPI client's `chat` domain. */
51
+ function adaptChatManager(client: TrUApiClient): ChatManager {
52
+ const chat = client.chat;
53
+ // Cache registration status by id so repeat calls don't re-prompt the host.
54
+ const roomStatus = new Map<string, ChatRoomRegistrationResult>();
55
+ const botStatus = new Map<string, ChatBotRegistrationResult>();
56
+
57
+ return {
58
+ async registerRoom(request) {
59
+ const cached = roomStatus.get(request.roomId);
60
+ if (cached) return cached;
61
+ const response = await unwrapHostResult(
62
+ chat.createRoom(request),
63
+ "chat registerRoom failed",
64
+ );
65
+ roomStatus.set(request.roomId, response.status);
66
+ return response.status;
67
+ },
68
+ async registerBot(request) {
69
+ const cached = botStatus.get(request.botId);
70
+ if (cached) return cached;
71
+ const response = await unwrapHostResult(
72
+ chat.registerBot(request),
73
+ "chat registerBot failed",
74
+ );
75
+ botStatus.set(request.botId, response.status);
76
+ return response.status;
77
+ },
78
+ async sendMessage(roomId, payload) {
79
+ const response = await unwrapHostResult(
80
+ chat.postMessage({ roomId, payload }),
81
+ "chat sendMessage failed",
82
+ );
83
+ return { messageId: response.messageId };
84
+ },
85
+ subscribeChatList(callback) {
86
+ return subscribeWithInterrupt(chat.listSubscribe(), (item) => callback(item.rooms));
87
+ },
88
+ subscribeAction(callback) {
89
+ return subscribeWithInterrupt(chat.actionSubscribe(), callback);
90
+ },
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Get the host chat manager, backed by `truApi.chat.*`. Returns `null` when
96
+ * running outside a host container.
97
+ *
98
+ * @returns The chat manager, or `null` if unavailable.
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * import { getChatManager } from "@parity/product-sdk-host";
103
+ *
104
+ * const chat = await getChatManager();
105
+ * if (chat) {
106
+ * await chat.registerBot({ botId: "echo", name: "Echo Bot", icon: "" });
107
+ * chat.subscribeAction((action) => { ... });
108
+ * }
109
+ * ```
110
+ */
111
+ export async function getChatManager(): Promise<ChatManager | null> {
112
+ const client = await getClient();
113
+ return client ? adaptChatManager(client) : null;
114
+ }
115
+
116
+ if (import.meta.vitest) {
117
+ const { test, expect } = import.meta.vitest;
118
+
119
+ test("getChatManager returns null outside a container", async () => {
120
+ expect(await getChatManager()).toBeNull();
121
+ });
122
+ }