@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/src/chain-spec.ts CHANGED
@@ -3,19 +3,21 @@
3
3
  /**
4
4
  * Higher-level wrapper for the host's chain-spec lookups.
5
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.
6
+ * The host exposes three separate chain-spec calls — `chain.getSpecGenesisHash`,
7
+ * `chain.getSpecChainName`, and `chain.getSpecProperties` — each reachable via
8
+ * {@link getTruApi} and each returning a neverthrow `ResultAsync`.
9
+ * {@link getChainSpec} fetches all three in one call and returns a single
10
+ * struct so callers read whichever field they need, matching the JSON-RPC
11
+ * `chainSpec_v1_*` family they mirror.
12
12
  *
13
13
  * @module
14
14
  */
15
15
 
16
16
  import { createLogger } from "@parity/product-sdk-logger";
17
17
 
18
- import { enumValue, formatHostError, getTruApi, type HexString } from "./truapi.js";
18
+ import type { HostError } from "./errors.js";
19
+ import { type Result, ok } from "./result.js";
20
+ import { getTruApi, type HexString, mapHostResult } from "./truapi.js";
19
21
 
20
22
  const log = createLogger("host:chain-spec");
21
23
 
@@ -25,8 +27,8 @@ const log = createLogger("host:chain-spec");
25
27
  *
26
28
  * The host returns this as a JSON string (mirroring the substrate
27
29
  * `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
+ * {@link getChainSpec} parses it into {@link ChainSpec.properties} and also
31
+ * surfaces the untouched JSON as {@link ChainSpec.propertiesRaw}. The well-known substrate fields are
30
32
  * typed for convenience; the index signature keeps any chain-specific extras
31
33
  * reachable without `any` at the call site.
32
34
  */
@@ -62,71 +64,78 @@ export interface ChainSpec {
62
64
  * Fetch a chain's full spec (genesis hash, name, and properties) from the host
63
65
  * in one call.
64
66
  *
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
67
+ * Issues the three underlying `chain.getSpec*` requests concurrently, unwraps
68
+ * each response, and parses the properties JSON. Note the `genesisHash` in the
69
+ * result is the value the host echoes back from `getSpecGenesisHash` for the
68
70
  * looked-up chain — pass the chain's known genesis hash as the lookup key.
69
71
  *
72
+ * `null` (outside a container) is preserved as an `ok` value — it is an
73
+ * expected state, not a failure — so callers branch on `r.ok && r.value`. A
74
+ * real host-call failure surfaces on the `err` channel.
75
+ *
70
76
  * @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`).
77
+ * @returns `ok(spec)` with the combined {@link ChainSpec}, `ok(null)` if the
78
+ * host is unavailable (running outside a container), or
79
+ * `err(HostCallFailedError)` if any underlying host call fails.
74
80
  *
75
81
  * @example
76
82
  * ```ts
77
83
  * import { getChainSpec } from "@parity/product-sdk-host";
78
84
  *
79
- * const spec = await getChainSpec(genesisHash);
80
- * if (spec) {
81
- * console.log(spec.name, spec.properties?.tokenSymbol);
85
+ * const r = await getChainSpec(genesisHash);
86
+ * if (r.ok && r.value) {
87
+ * console.log(r.value.name, r.value.properties?.tokenSymbol);
82
88
  * }
83
89
  * ```
84
90
  */
85
- export async function getChainSpec(genesisHash: HexString): Promise<ChainSpec | null> {
91
+ export async function getChainSpec(
92
+ genesisHash: HexString,
93
+ ): Promise<Result<ChainSpec | null, HostError>> {
86
94
  const truApi = await getTruApi();
87
95
  if (!truApi) {
88
96
  log.debug("getChainSpec: TruAPI unavailable");
89
- return null;
97
+ return ok(null);
90
98
  }
91
99
  log.debug("getChainSpec", { genesisHash });
92
100
 
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
- },
101
+ const [genesisHashResult, nameResult, propertiesResult] = await Promise.all([
102
+ mapHostResult(
103
+ truApi.chain.getSpecGenesisHash({ genesisHash }),
104
+ (response) => response.genesisHash,
105
+ "getChainSpec (genesisHash) failed",
102
106
  ),
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
- },
107
+ mapHostResult(
108
+ truApi.chain.getSpecChainName({ genesisHash }),
109
+ (response) => response.chainName,
110
+ "getChainSpec (chainName) failed",
110
111
  ),
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
- },
112
+ mapHostResult(
113
+ truApi.chain.getSpecProperties({ genesisHash }),
114
+ (response) => response.properties,
115
+ "getChainSpec (properties) failed",
118
116
  ),
119
117
  ]);
120
118
 
119
+ // Short-circuit on the first failing call.
120
+ if (!genesisHashResult.ok) return genesisHashResult;
121
+ if (!nameResult.ok) return nameResult;
122
+ if (!propertiesResult.ok) return propertiesResult;
123
+
124
+ const propertiesRaw = propertiesResult.value;
121
125
  let properties: ChainProperties | null;
122
126
  try {
123
127
  properties = JSON.parse(propertiesRaw) as ChainProperties;
124
- } catch (err) {
125
- log.debug("getChainSpec: properties JSON parse failed", err);
128
+ } catch (parseError) {
129
+ log.debug("getChainSpec: properties JSON parse failed", parseError);
126
130
  properties = null;
127
131
  }
128
132
 
129
- return { genesisHash: resolvedGenesisHash, name, properties, propertiesRaw };
133
+ return ok({
134
+ genesisHash: genesisHashResult.value,
135
+ name: nameResult.value,
136
+ properties,
137
+ propertiesRaw,
138
+ });
130
139
  }
131
140
 
132
141
  if (import.meta.vitest) {
@@ -134,9 +143,11 @@ if (import.meta.vitest) {
134
143
 
135
144
  async function withMockedTruApi<T>(
136
145
  bridge: {
137
- chainSpecGenesisHash?: (req: unknown) => unknown;
138
- chainSpecChainName?: (req: unknown) => unknown;
139
- chainSpecProperties?: (req: unknown) => unknown;
146
+ chain?: {
147
+ getSpecGenesisHash?: (req: unknown) => unknown;
148
+ getSpecChainName?: (req: unknown) => unknown;
149
+ getSpecProperties?: (req: unknown) => unknown;
150
+ };
140
151
  } | null,
141
152
  fn: (mod: typeof import("./chain-spec.js")) => Promise<T>,
142
153
  ): Promise<T> {
@@ -146,7 +157,6 @@ if (import.meta.vitest) {
146
157
  return {
147
158
  ...original,
148
159
  getTruApi: async () => bridge,
149
- enumValue: (version: string, value: unknown) => ({ tag: version, value }),
150
160
  };
151
161
  });
152
162
  try {
@@ -158,35 +168,47 @@ if (import.meta.vitest) {
158
168
  }
159
169
  }
160
170
 
161
- const ok = (value: unknown) => ({
162
- match: async (onOk: (v: unknown) => unknown) => onOk({ tag: "v1", value }),
171
+ /** A resolved ResultAsync stub yielding the given response object. */
172
+ const okAsync = (response: unknown) => ({
173
+ match: async (onOk: (v: unknown) => unknown) => onOk(response),
163
174
  });
164
175
 
165
176
  describe("getChainSpec", () => {
166
- test("returns null when TruAPI is unavailable", async () => {
177
+ test("returns ok(null) when TruAPI is unavailable", async () => {
167
178
  await withMockedTruApi(null, async (mod) => {
168
- expect(await mod.getChainSpec("0x00")).toBeNull();
179
+ expect(await mod.getChainSpec("0x00")).toEqual({ ok: true, value: null });
169
180
  });
170
181
  });
171
182
 
172
183
  test("combines the three calls and parses properties JSON", async () => {
173
184
  await withMockedTruApi(
174
185
  {
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"}'),
186
+ chain: {
187
+ getSpecGenesisHash: vi
188
+ .fn()
189
+ .mockReturnValue(okAsync({ genesisHash: "0xabcd" })),
190
+ getSpecChainName: vi
191
+ .fn()
192
+ .mockReturnValue(okAsync({ chainName: "Polkadot" })),
193
+ getSpecProperties: vi.fn().mockReturnValue(
194
+ okAsync({
195
+ properties:
196
+ '{"ss58Format":0,"tokenDecimals":10,"tokenSymbol":"DOT"}',
197
+ }),
181
198
  ),
199
+ },
182
200
  },
183
201
  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"}',
202
+ const result = await mod.getChainSpec("0xabcd");
203
+ expect(result).toEqual({
204
+ ok: true,
205
+ value: {
206
+ genesisHash: "0xabcd",
207
+ name: "Polkadot",
208
+ properties: { ss58Format: 0, tokenDecimals: 10, tokenSymbol: "DOT" },
209
+ propertiesRaw:
210
+ '{"ss58Format":0,"tokenDecimals":10,"tokenSymbol":"DOT"}',
211
+ },
190
212
  });
191
213
  },
192
214
  );
@@ -195,34 +217,54 @@ if (import.meta.vitest) {
195
217
  test("leaves properties null when the JSON is malformed", async () => {
196
218
  await withMockedTruApi(
197
219
  {
198
- chainSpecGenesisHash: vi.fn().mockReturnValue(ok("0xabcd")),
199
- chainSpecChainName: vi.fn().mockReturnValue(ok("Polkadot")),
200
- chainSpecProperties: vi.fn().mockReturnValue(ok("not json")),
220
+ chain: {
221
+ getSpecGenesisHash: vi
222
+ .fn()
223
+ .mockReturnValue(okAsync({ genesisHash: "0xabcd" })),
224
+ getSpecChainName: vi
225
+ .fn()
226
+ .mockReturnValue(okAsync({ chainName: "Polkadot" })),
227
+ getSpecProperties: vi
228
+ .fn()
229
+ .mockReturnValue(okAsync({ properties: "not json" })),
230
+ },
201
231
  },
202
232
  async (mod) => {
203
- const spec = await mod.getChainSpec("0xabcd");
204
- expect(spec?.properties).toBeNull();
205
- expect(spec?.propertiesRaw).toBe("not json");
233
+ const result = await mod.getChainSpec("0xabcd");
234
+ expect(result.ok).toBe(true);
235
+ if (result.ok) {
236
+ expect(result.value?.properties).toBeNull();
237
+ expect(result.value?.propertiesRaw).toBe("not json");
238
+ }
206
239
  },
207
240
  );
208
241
  });
209
242
 
210
- test("wraps host errors with a diagnostic message", async () => {
243
+ test("returns err(HostCallFailedError) when a host call fails", async () => {
211
244
  await withMockedTruApi(
212
245
  {
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("{}")),
246
+ chain: {
247
+ getSpecGenesisHash: vi.fn().mockReturnValue({
248
+ match: async (
249
+ _onOk: (v: unknown) => unknown,
250
+ onErr: (e: unknown) => unknown,
251
+ ) => onErr({ reason: "boom" }),
252
+ }),
253
+ getSpecChainName: vi
254
+ .fn()
255
+ .mockReturnValue(okAsync({ chainName: "Polkadot" })),
256
+ getSpecProperties: vi.fn().mockReturnValue(okAsync({ properties: "{}" })),
257
+ },
221
258
  },
222
259
  async (mod) => {
223
- await expect(mod.getChainSpec("0xabcd")).rejects.toThrow(
224
- /getChainSpec \(genesisHash\) failed: GenericError: boom/,
225
- );
260
+ const result = await mod.getChainSpec("0xabcd");
261
+ expect(result.ok).toBe(false);
262
+ if (!result.ok) {
263
+ expect(result.error.name).toBe("HostCallFailedError");
264
+ expect(result.error.message).toMatch(
265
+ /getChainSpec \(genesisHash\) failed: boom/,
266
+ );
267
+ }
226
268
  },
227
269
  );
228
270
  });
@@ -3,97 +3,92 @@
3
3
  /**
4
4
  * Higher-level wrappers for the host's transaction broadcast lifecycle.
5
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.
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
12
  *
13
13
  * @module
14
14
  */
15
15
 
16
16
  import { createLogger } from "@parity/product-sdk-logger";
17
17
 
18
- import { enumValue, formatHostError, getTruApi, type HexString } from "./truapi.js";
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";
19
21
 
20
22
  const log = createLogger("host:chain-transaction");
21
23
 
22
24
  /**
23
25
  * Broadcast a signed transaction to the network via the host.
24
26
  *
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.
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.
29
30
  *
30
31
  * @param genesisHash - The `0x`-prefixed genesis hash of the target chain.
31
32
  * @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`).
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)`.
35
36
  *
36
37
  * @example
37
38
  * ```ts
38
39
  * import { broadcastTransaction, stopTransaction } from "@parity/product-sdk-host";
39
40
  *
40
- * const operationId = await broadcastTransaction(genesisHash, signedTx);
41
+ * const r = await broadcastTransaction(genesisHash, signedTx);
41
42
  * // later, to stop re-broadcasting:
42
- * if (operationId) await stopTransaction(genesisHash, operationId);
43
+ * if (r.ok && r.value) await stopTransaction(genesisHash, r.value);
43
44
  * ```
44
45
  */
45
46
  export async function broadcastTransaction(
46
47
  genesisHash: HexString,
47
48
  transaction: HexString,
48
- ): Promise<string | null> {
49
+ ): Promise<Result<string | null, HostError>> {
49
50
  const truApi = await getTruApi();
50
51
  if (!truApi) {
51
- throw new Error("broadcastTransaction: TruAPI unavailable");
52
+ return err(new HostUnavailableError("broadcastTransaction: TruAPI unavailable"));
52
53
  }
53
54
  log.debug("broadcastTransaction", { genesisHash });
54
55
 
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
- );
56
+ return mapHostResult(
57
+ truApi.chain.broadcastTransaction({ genesisHash, transaction }),
58
+ (response) => response.operationId ?? null,
59
+ "broadcastTransaction failed",
60
+ );
66
61
  }
67
62
 
68
63
  /**
69
64
  * Stop an in-flight broadcast started by {@link broadcastTransaction}.
70
65
  *
71
- * Builds the `v1` envelope, calls `hostApi.chainTransactionStop`, and unwraps
72
- * the response.
66
+ * Calls `truApi.chain.stopTransaction` and unwraps the response.
73
67
  *
74
68
  * @param genesisHash - The `0x`-prefixed genesis hash of the target chain.
75
69
  * @param operationId - The operation id returned by
76
70
  * {@link broadcastTransaction}.
77
- * @throws If the host is unavailable or the stop fails (`GenericError`).
71
+ * @returns `ok` on success, or `err(HostUnavailableError | HostCallFailedError)`.
78
72
  *
79
73
  * @example
80
74
  * ```ts
81
75
  * await stopTransaction(genesisHash, operationId);
82
76
  * ```
83
77
  */
84
- export async function stopTransaction(genesisHash: HexString, operationId: string): Promise<void> {
78
+ export async function stopTransaction(
79
+ genesisHash: HexString,
80
+ operationId: string,
81
+ ): Promise<Result<void, HostError>> {
85
82
  const truApi = await getTruApi();
86
83
  if (!truApi) {
87
- throw new Error("stopTransaction: TruAPI unavailable");
84
+ return err(new HostUnavailableError("stopTransaction: TruAPI unavailable"));
88
85
  }
89
86
  log.debug("stopTransaction", { genesisHash, operationId });
90
87
 
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
- },
88
+ return mapHostResult(
89
+ truApi.chain.stopTransaction({ genesisHash, operationId }),
90
+ () => undefined,
91
+ "stopTransaction failed",
97
92
  );
98
93
  }
99
94
 
@@ -102,8 +97,10 @@ if (import.meta.vitest) {
102
97
 
103
98
  async function withMockedTruApi<T>(
104
99
  bridge: {
105
- chainTransactionBroadcast?: (req: unknown) => unknown;
106
- chainTransactionStop?: (req: unknown) => unknown;
100
+ chain?: {
101
+ broadcastTransaction?: (req: unknown) => unknown;
102
+ stopTransaction?: (req: unknown) => unknown;
103
+ };
107
104
  } | null,
108
105
  fn: (mod: typeof import("./chain-transaction.js")) => Promise<T>,
109
106
  ): Promise<T> {
@@ -113,7 +110,6 @@ if (import.meta.vitest) {
113
110
  return {
114
111
  ...original,
115
112
  getTruApi: async () => bridge,
116
- enumValue: (version: string, value: unknown) => ({ tag: version, value }),
117
113
  };
118
114
  });
119
115
  try {
@@ -125,86 +121,119 @@ if (import.meta.vitest) {
125
121
  }
126
122
  }
127
123
 
128
- const ok = (value: unknown) => ({
129
- match: async (onOk: (v: unknown) => unknown) => onOk({ tag: "v1", value }),
124
+ /** A resolved ResultAsync stub yielding the given response object. */
125
+ const ok = (response: unknown) => ({
126
+ match: async (onOk: (v: unknown) => unknown) => onOk(response),
130
127
  });
131
- const errResult = (name: string, message: string) => ({
128
+ /** A rejected ResultAsync stub yielding a truapi `GenericError` (`{ reason }`). */
129
+ const errResult = (reason: string) => ({
132
130
  match: async (_onOk: (v: unknown) => unknown, onErr: (e: unknown) => unknown) =>
133
- onErr({ tag: "v1", value: { name, message } }),
131
+ onErr({ reason }),
134
132
  });
135
133
 
136
134
  describe("broadcastTransaction", () => {
137
- test("throws when TruAPI is unavailable", async () => {
135
+ test("returns err(HostUnavailableError) when TruAPI is unavailable", async () => {
138
136
  await withMockedTruApi(null, async (mod) => {
139
- await expect(mod.broadcastTransaction("0x00", "0x01")).rejects.toThrow(
140
- /TruAPI unavailable/,
141
- );
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
142
  });
143
143
  });
144
144
 
145
- test("unwraps the operation id", async () => {
145
+ test("returns ok with the operation id", async () => {
146
146
  await withMockedTruApi(
147
- { chainTransactionBroadcast: vi.fn().mockReturnValue(ok("op-1")) },
147
+ {
148
+ chain: {
149
+ broadcastTransaction: vi.fn().mockReturnValue(ok({ operationId: "op-1" })),
150
+ },
151
+ },
148
152
  async (mod) => {
149
- expect(await mod.broadcastTransaction("0x00", "0x01")).toBe("op-1");
153
+ expect(await mod.broadcastTransaction("0x00", "0x01")).toEqual({
154
+ ok: true,
155
+ value: "op-1",
156
+ });
150
157
  },
151
158
  );
152
159
  });
153
160
 
154
- test("passes through a null operation id", async () => {
161
+ test("passes through a missing operation id as ok(null)", async () => {
155
162
  await withMockedTruApi(
156
- { chainTransactionBroadcast: vi.fn().mockReturnValue(ok(null)) },
163
+ {
164
+ chain: {
165
+ broadcastTransaction: vi.fn().mockReturnValue(ok({})),
166
+ },
167
+ },
157
168
  async (mod) => {
158
- expect(await mod.broadcastTransaction("0x00", "0x01")).toBeNull();
169
+ expect(await mod.broadcastTransaction("0x00", "0x01")).toEqual({
170
+ ok: true,
171
+ value: null,
172
+ });
159
173
  },
160
174
  );
161
175
  });
162
176
 
163
- test("wraps host errors with a diagnostic message", async () => {
177
+ test("wraps host errors in err(HostCallFailedError) with a diagnostic message", async () => {
164
178
  await withMockedTruApi(
165
179
  {
166
- chainTransactionBroadcast: vi
167
- .fn()
168
- .mockReturnValue(errResult("GenericError", "boom")),
180
+ chain: {
181
+ broadcastTransaction: vi.fn().mockReturnValue(errResult("boom")),
182
+ },
169
183
  },
170
184
  async (mod) => {
171
- await expect(mod.broadcastTransaction("0x00", "0x01")).rejects.toThrow(
172
- /broadcastTransaction failed: GenericError: boom/,
173
- );
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
+ }
174
191
  },
175
192
  );
176
193
  });
177
194
  });
178
195
 
179
196
  describe("stopTransaction", () => {
180
- test("throws when TruAPI is unavailable", async () => {
197
+ test("returns err(HostUnavailableError) when TruAPI is unavailable", async () => {
181
198
  await withMockedTruApi(null, async (mod) => {
182
- await expect(mod.stopTransaction("0x00", "op-1")).rejects.toThrow(
183
- /TruAPI unavailable/,
184
- );
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
+ }
185
204
  });
186
205
  });
187
206
 
188
- test("resolves on the v1 success envelope", async () => {
207
+ test("returns ok on success", async () => {
189
208
  await withMockedTruApi(
190
- { chainTransactionStop: vi.fn().mockReturnValue(ok(undefined)) },
209
+ {
210
+ chain: {
211
+ stopTransaction: vi.fn().mockReturnValue(ok(undefined)),
212
+ },
213
+ },
191
214
  async (mod) => {
192
- await expect(mod.stopTransaction("0x00", "op-1")).resolves.toBeUndefined();
215
+ expect(await mod.stopTransaction("0x00", "op-1")).toEqual({
216
+ ok: true,
217
+ value: undefined,
218
+ });
193
219
  },
194
220
  );
195
221
  });
196
222
 
197
- test("wraps host errors with a diagnostic message", async () => {
223
+ test("wraps host errors in err(HostCallFailedError) with a diagnostic message", async () => {
198
224
  await withMockedTruApi(
199
225
  {
200
- chainTransactionStop: vi
201
- .fn()
202
- .mockReturnValue(errResult("GenericError", "boom")),
226
+ chain: {
227
+ stopTransaction: vi.fn().mockReturnValue(errResult("boom")),
228
+ },
203
229
  },
204
230
  async (mod) => {
205
- await expect(mod.stopTransaction("0x00", "op-1")).rejects.toThrow(
206
- /stopTransaction failed: GenericError: boom/,
207
- );
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
+ }
208
237
  },
209
238
  );
210
239
  });