@parity/product-sdk-host 0.15.0 → 0.16.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,287 @@
1
+ // Copyright 2026 Parity Technologies (UK) Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Host chain discovery. Resolves chain roles to genesis hashes against the
5
+ * host's configured environment instead of hard-coding them.
6
+ *
7
+ * The wire method takes one identifier per call, so the facade fires one
8
+ * concurrent call per requested identifier and caches the combined result
9
+ * for the lifetime of the connection. Consumed internally by chain-client.
10
+ * Products normally never call this directly.
11
+ *
12
+ * @module
13
+ */
14
+
15
+ import type {
16
+ ChainIdentifier,
17
+ HexString,
18
+ TrUApiClient,
19
+ VersionedRemoteChainInfoError,
20
+ scale,
21
+ } from "@parity/truapi";
22
+ import { createLogger } from "@parity/product-sdk-logger";
23
+ import { formatHostError } from "./errors.js";
24
+ import { getClient } from "./transport.js";
25
+
26
+ const log = createLogger("host");
27
+
28
+ /**
29
+ * Chain-role identifier. A closed protocol enum, not a free-form name. The
30
+ * host maps each role to the concrete chain of its configured environment.
31
+ */
32
+ export type HostChainIdentifier = ChainIdentifier;
33
+
34
+ /** The host's configured environment plus per-identifier resolved genesis hashes. */
35
+ export interface HostChainDiscovery {
36
+ /** Ecosystem the host is configured for, e.g. `"polkadot"`, `"paseo"`. */
37
+ network: string;
38
+ /** Present for every requested identifier the host serves. */
39
+ chains: Partial<Record<HostChainIdentifier, HexString>>;
40
+ }
41
+
42
+ /** Error channel of `chain.getChainInfo`. */
43
+ type GetChainInfoError = scale.CallErrorValue<VersionedRemoteChainInfoError>;
44
+
45
+ /**
46
+ * Marks a transient probe failure. These are evicted from the cache so the
47
+ * next call re-probes. Stable "no discovery" answers stay cached.
48
+ */
49
+ const TRANSIENT_FAILURE = Symbol("transient-failure");
50
+
51
+ /**
52
+ * Hosts that predate the wire-id reservation never answer the probe at all,
53
+ * so a silent host must resolve as "no discovery" instead of hanging. The
54
+ * answer comes from host config with no chain I/O, so a short deadline is
55
+ * enough. A timeout is treated as transient, never cached: a host that
56
+ * supports discovery but started slowly would otherwise be recorded as
57
+ * pre-discovery for the life of the client. The cost is that a genuinely
58
+ * legacy host pays the deadline again on the next call.
59
+ */
60
+ const PROBE_TIMEOUT_MS = 3_000;
61
+
62
+ const discoveryCache = new WeakMap<TrUApiClient, Map<string, Promise<HostChainDiscovery | null>>>();
63
+
64
+ /**
65
+ * Resolve chain roles against the current host.
66
+ *
67
+ * Returns `null` when discovery is unavailable: outside a container, on a
68
+ * legacy host, or when the host serves none of the requested identifiers.
69
+ * Callers treat `null` as "fall back to configured constants".
70
+ *
71
+ * One concurrent `getChainInfo` call is made per identifier. Identifiers
72
+ * the host answers `NotSupported` for are absent from `chains`. Stable
73
+ * answers are cached per client and identifier set. Unexpected wire failures
74
+ * and probe timeouts are logged, return `null` and are not cached, so a later
75
+ * call re-probes.
76
+ */
77
+ export async function getHostChainInfo(
78
+ identifiers: readonly HostChainIdentifier[],
79
+ ): Promise<HostChainDiscovery | null> {
80
+ const client = await getClient();
81
+ if (!client) return null;
82
+ let bySet = discoveryCache.get(client);
83
+ if (!bySet) {
84
+ bySet = new Map();
85
+ discoveryCache.set(client, bySet);
86
+ }
87
+ const key = [...identifiers].sort().join(",");
88
+ let cached = bySet.get(key);
89
+ if (!cached) {
90
+ cached = fetchChainInfo(client, identifiers).then((result) => {
91
+ if (result === TRANSIENT_FAILURE) {
92
+ // Evict so the next caller re-probes.
93
+ bySet.delete(key);
94
+ return null;
95
+ }
96
+ return result;
97
+ });
98
+ bySet.set(key, cached);
99
+ }
100
+ return cached;
101
+ }
102
+
103
+ async function fetchChainInfo(
104
+ client: TrUApiClient,
105
+ identifiers: readonly HostChainIdentifier[],
106
+ ): Promise<HostChainDiscovery | null | typeof TRANSIENT_FAILURE> {
107
+ try {
108
+ let timer: ReturnType<typeof setTimeout> | undefined;
109
+ const probe = Promise.all(
110
+ identifiers.map((id) =>
111
+ client.chain.getChainInfo({ chain: id }).match(
112
+ (value) => ({ id, ok: value }) as const,
113
+ (error) => ({ id, err: error }) as const,
114
+ ),
115
+ ),
116
+ );
117
+ const outcomes = await Promise.race([
118
+ probe,
119
+ new Promise<"timeout">((resolve) => {
120
+ timer = setTimeout(() => resolve("timeout"), PROBE_TIMEOUT_MS);
121
+ }),
122
+ ]).finally(() => clearTimeout(timer));
123
+ if (outcomes === "timeout") {
124
+ log.warn("getChainInfo probe timed out, treating the host as pre-discovery for now");
125
+ return TRANSIENT_FAILURE;
126
+ }
127
+ let network: string | undefined;
128
+ const chains: Partial<Record<HostChainIdentifier, HexString>> = {};
129
+ for (const outcome of outcomes) {
130
+ if ("ok" in outcome) {
131
+ network = outcome.ok.network;
132
+ chains[outcome.id] = outcome.ok.genesisHash;
133
+ continue;
134
+ }
135
+ // "Unsupported" means the host predates the method entirely.
136
+ // "NotSupported" means this one identifier is not served.
137
+ if (outcome.err.tag === "Unsupported") return null;
138
+ if (isNotSupported(outcome.err)) continue;
139
+ log.warn(`getChainInfo failed: ${formatHostError(outcome.err)}`);
140
+ return TRANSIENT_FAILURE;
141
+ }
142
+ // Every identifier was refused, so the host never revealed its network.
143
+ if (network === undefined) return null;
144
+ return { network, chains };
145
+ } catch (error) {
146
+ log.warn(`getChainInfo failed: ${formatHostError(error)}`);
147
+ return TRANSIENT_FAILURE;
148
+ }
149
+ }
150
+
151
+ /** True when a domain error is the unit `NotSupported` variant. */
152
+ function isNotSupported(error: GetChainInfoError): boolean {
153
+ return error.tag === "Domain" && error.value.value.tag === "NotSupported";
154
+ }
155
+
156
+ if (import.meta.vitest) {
157
+ const { test, expect, afterEach, vi } = import.meta.vitest;
158
+ const { setTruApiClient } = await import("./transport.js");
159
+ type ChainInfoResponse = import("@parity/truapi").RemoteChainInfoResponse;
160
+
161
+ type Served = Partial<Record<HostChainIdentifier, HexString>>;
162
+
163
+ const NOT_SUPPORTED: GetChainInfoError = {
164
+ tag: "Domain",
165
+ value: { tag: "V1", value: { tag: "NotSupported" } },
166
+ };
167
+
168
+ type FakeBehavior = { network: string; served: Served } | { err: GetChainInfoError };
169
+
170
+ /** Fake client answering from a served-chains map or a per-call behavior function. */
171
+ function fakeClient(
172
+ behavior: FakeBehavior | ((call: number) => FakeBehavior),
173
+ calls: { count: number; requests: HostChainIdentifier[] } = { count: 0, requests: [] },
174
+ ): TrUApiClient {
175
+ return {
176
+ chain: {
177
+ getChainInfo: (request: { chain: HostChainIdentifier }) => {
178
+ const current =
179
+ typeof behavior === "function" ? behavior(calls.count) : behavior;
180
+ calls.count += 1;
181
+ calls.requests.push(request.chain);
182
+ return {
183
+ match: async <A, B>(
184
+ onOk: (v: ChainInfoResponse) => A,
185
+ onErr: (e: GetChainInfoError) => B,
186
+ ) => {
187
+ if ("err" in current) return onErr(current.err);
188
+ const genesisHash = current.served[request.chain];
189
+ if (!genesisHash) return onErr(NOT_SUPPORTED);
190
+ return onOk({
191
+ network: current.network,
192
+ chain: request.chain,
193
+ genesisHash,
194
+ });
195
+ },
196
+ };
197
+ },
198
+ },
199
+ } as unknown as TrUApiClient;
200
+ }
201
+
202
+ const PASEO_SERVED: Served = {
203
+ AssetHub: "0xaa" as HexString,
204
+ Bulletin: "0xbb" as HexString,
205
+ People: "0xcc" as HexString,
206
+ };
207
+
208
+ afterEach(() => {
209
+ setTruApiClient(null);
210
+ vi.restoreAllMocks();
211
+ });
212
+
213
+ test("resolves each requested identifier once, unserved ones absent", async () => {
214
+ const calls = { count: 0, requests: [] as HostChainIdentifier[] };
215
+ setTruApiClient(
216
+ fakeClient({ network: "paseo", served: { AssetHub: "0xaa" as HexString } }, calls),
217
+ );
218
+ const result = await getHostChainInfo(["AssetHub", "Bulletin", "People"]);
219
+ expect(result).toEqual({ network: "paseo", chains: { AssetHub: "0xaa" } });
220
+ expect(calls.requests).toEqual(["AssetHub", "Bulletin", "People"]);
221
+ });
222
+
223
+ test("returns null when discovery is unavailable and caches the answer", async () => {
224
+ // Outside a container.
225
+ expect(await getHostChainInfo(["AssetHub"])).toBeNull();
226
+ // Every identifier refused, so the network is never revealed.
227
+ const refused = { count: 0, requests: [] as HostChainIdentifier[] };
228
+ setTruApiClient(fakeClient({ network: "devnet", served: {} }, refused));
229
+ expect(await getHostChainInfo(["Bulletin"])).toBeNull();
230
+ expect(await getHostChainInfo(["Bulletin"])).toBeNull();
231
+ expect(refused.count).toBe(1);
232
+ // Legacy host answering Unsupported.
233
+ const legacy = { count: 0, requests: [] as HostChainIdentifier[] };
234
+ setTruApiClient(fakeClient({ err: { tag: "Unsupported" } }, legacy));
235
+ expect(await getHostChainInfo(["AssetHub"])).toBeNull();
236
+ expect(await getHostChainInfo(["AssetHub"])).toBeNull();
237
+ expect(legacy.count).toBe(1);
238
+ });
239
+
240
+ test("caches per client and identifier set, ignoring order", async () => {
241
+ const calls = { count: 0, requests: [] as HostChainIdentifier[] };
242
+ setTruApiClient(fakeClient({ network: "paseo", served: PASEO_SERVED }, calls));
243
+ await getHostChainInfo(["AssetHub", "Bulletin"]);
244
+ await getHostChainInfo(["Bulletin", "AssetHub"]);
245
+ expect(calls.count).toBe(2);
246
+ await getHostChainInfo(["People"]);
247
+ expect(calls.count).toBe(3);
248
+ });
249
+
250
+ test("a silent host times out to null and the next call re-probes", async () => {
251
+ vi.useFakeTimers();
252
+ try {
253
+ const calls = { count: 0 };
254
+ setTruApiClient({
255
+ chain: {
256
+ getChainInfo: () => {
257
+ calls.count += 1;
258
+ return { match: () => new Promise(() => {}) };
259
+ },
260
+ },
261
+ } as unknown as TrUApiClient);
262
+ const pending = getHostChainInfo(["AssetHub"]);
263
+ await vi.advanceTimersByTimeAsync(3_000);
264
+ expect(await pending).toBeNull();
265
+ // A host that merely started slowly must not stay classified as
266
+ // pre-discovery, so the timeout is never cached.
267
+ const retry = getHostChainInfo(["AssetHub"]);
268
+ await vi.advanceTimersByTimeAsync(3_000);
269
+ expect(await retry).toBeNull();
270
+ expect(calls.count).toBe(2);
271
+ } finally {
272
+ vi.useRealTimers();
273
+ }
274
+ });
275
+
276
+ test("transient wire failures return null but are not cached, the next call re-probes", async () => {
277
+ setTruApiClient(
278
+ fakeClient((call) =>
279
+ call === 0
280
+ ? { err: { tag: "HostFailure", value: { reason: "boom" } } }
281
+ : { network: "paseo", served: PASEO_SERVED },
282
+ ),
283
+ );
284
+ expect(await getHostChainInfo(["AssetHub"])).toBeNull();
285
+ expect((await getHostChainInfo(["AssetHub"]))?.network).toBe("paseo");
286
+ });
287
+ }
package/src/index.ts CHANGED
@@ -53,6 +53,10 @@ export type {
53
53
  RemotePermission,
54
54
  } from "./truapi.js";
55
55
 
56
+ // Host chain discovery.
57
+ export { getHostChainInfo } from "./chain-discovery.js";
58
+ export type { HostChainDiscovery, HostChainIdentifier } from "./chain-discovery.js";
59
+
56
60
  // Result type + typed host errors (the throw→Result boundary)
57
61
  export { ok, err } from "./result.js";
58
62
  export type { Result } from "./result.js";
@@ -67,16 +71,23 @@ export {
67
71
  export type { HostErrorPayload } from "./errors.js";
68
72
 
69
73
  // Accounts — host wallet accounts, product accounts, Ring VRF, and signers.
70
- export { getAccountsProvider } from "./accounts.js";
74
+ export { getAccountsProvider, findRingVrfKeyHandle } from "./accounts.js";
71
75
  export type {
72
76
  AccountsProvider,
73
77
  DerivationIndex,
74
78
  HostAccount,
75
79
  ProductAccount,
80
+ ProductAccountLookup,
76
81
  ContextualAlias,
77
82
  ProductProofContext,
83
+ RegisteredRingVrfKey,
78
84
  RingLocation,
79
85
  RingVRFProof,
86
+ RingVrfKeyDisclosure,
87
+ RingVrfKeyHandle,
88
+ RingVrfPublicKey,
89
+ VrfSignature,
90
+ VrfTranscriptItem,
80
91
  } from "./accounts.js";
81
92
 
82
93
  // Higher-level permission wrappers
package/src/payments.ts CHANGED
@@ -97,7 +97,7 @@ function adaptPaymentManager(client: TrUApiClient): PaymentManager {
97
97
  * const payments = await getPaymentManager();
98
98
  * if (payments) {
99
99
  * const sub = payments.subscribeBalance((b) => { ... });
100
- * await payments.topUp(1_000_000n, { tag: "ProductAccount", value: { derivationIndex: { tag: "Left", value: 0 } } });
100
+ * await payments.topUp(1_000_000n, { tag: "ProductAccount", value: { derivationIndex: { tag: "Index", value: 0 } } });
101
101
  * const { id } = await payments.requestPayment(500n, "0x…");
102
102
  * sub.unsubscribe();
103
103
  * }
package/src/testing.ts CHANGED
@@ -10,17 +10,21 @@
10
10
  * makes a default `SignerManager`, `local-storage` auto-detection, and the
11
11
  * `statement-store` / `cloud-storage` host paths testable.
12
12
  *
13
- * Not modeled: the PAPI `chain` JSON-RPC surface behind `getHostProvider()`
14
- * there's no chain-read fake, by design; the host owns RPC selection — and the
15
- * `chat` / `coinPayment` / `entropy` / `notifications` / `payment` /
16
- * `permissions` / `resourceAllocation` / `theme` domains. Touching an unmodeled
17
- * domain throws a descriptive error rather than failing with `undefined is not
18
- * a function`.
13
+ * Of the `chain` domain only `getChainInfo` is modeled, so host chain discovery
14
+ * (and `getChainAPI()` on top of it) resolves in tests; see the `chainInfo`
15
+ * option. Not modeled: the rest of the PAPI `chain` JSON-RPC surface behind
16
+ * `getHostProvider()` there's no chain-read fake, by design; the host owns RPC
17
+ * selection and the `chat` / `coinPayment` / `entropy` / `notifications` /
18
+ * `payment` / `permissions` / `resourceAllocation` / `theme` domains. Touching
19
+ * an unmodeled domain throws a descriptive error rather than failing with
20
+ * `undefined is not a function`.
19
21
  *
20
22
  * @packageDocumentation
21
23
  */
22
24
  import type { ObservableLike, Observer, Subscription, TrUApiClient } from "@parity/truapi";
23
- import { okAsync } from "neverthrow";
25
+ import { errAsync, okAsync } from "neverthrow";
26
+
27
+ import type { HostChainIdentifier } from "./chain-discovery.js";
24
28
 
25
29
  import { setTruApiClient } from "./transport.js";
26
30
 
@@ -91,10 +95,17 @@ function oneShotObservable<Item>(item: Item): ObservableLike<Item> {
91
95
  * member access throws with a pointer here, instead of the bare TypeError an
92
96
  * empty stub would give. The empty-object cast is the one concession a Proxy
93
97
  * needs; every modeled domain is checked structurally.
98
+ *
99
+ * `modeled` carries the members that _are_ implemented, for a domain where the
100
+ * fake covers some calls but not the whole surface.
94
101
  */
95
- function notModeled<D extends keyof TrUApiClient>(domain: D): PublicSurface<TrUApiClient[D]> {
96
- return new Proxy({} as PublicSurface<TrUApiClient[D]>, {
97
- get(_target, member) {
102
+ function notModeled<D extends keyof TrUApiClient>(
103
+ domain: D,
104
+ modeled?: Partial<PublicSurface<TrUApiClient[D]>>,
105
+ ): PublicSurface<TrUApiClient[D]> {
106
+ return new Proxy((modeled ?? {}) as PublicSurface<TrUApiClient[D]>, {
107
+ get(target, member) {
108
+ if (member in target) return target[member as keyof typeof target];
98
109
  // Stay quiet for inspection probes (console.log, await-resolution).
99
110
  if (typeof member === "symbol" || member === "then") return undefined;
100
111
  throw new Error(
@@ -114,6 +125,22 @@ function preimageKey(hexValue: string): `0x${string}` {
114
125
  return `0x${(h >>> 0).toString(16).padStart(8, "0")}`;
115
126
  }
116
127
 
128
+ /**
129
+ * What a fake host reports through `chain.getChainInfo`: the network id it is
130
+ * configured for, and a genesis hash per chain role it serves.
131
+ *
132
+ * Roles left out are answered `NotSupported`, exactly as a host that does not
133
+ * serve them. Use the genesis hashes the descriptors expose (e.g.
134
+ * `paseo_asset_hub.genesis`) if the test drives `getChainAPI()`, since the
135
+ * environment is derived by matching the asset hub genesis against the bundle.
136
+ */
137
+ export interface FakeChainInfo {
138
+ /** Ecosystem the fake host claims, e.g. `"paseo"`. */
139
+ network: string;
140
+ /** Genesis hash per chain role served. */
141
+ chains: Partial<Record<HostChainIdentifier, `0x${string}`>>;
142
+ }
143
+
117
144
  /** Options for {@link createFakeTruApiClient}. */
118
145
  export interface CreateFakeTruApiClientOptions {
119
146
  /** `account.getUserId` primary username. Default `"alice.dot"`. */
@@ -130,6 +157,12 @@ export interface CreateFakeTruApiClientOptions {
130
157
  legacyAccounts?: Array<{ publicKey: Uint8Array; name: string }>;
131
158
  /** Seed the in-memory preimage store, keyed by the `0x` preimage key. */
132
159
  preimages?: Record<string, Uint8Array>;
160
+ /**
161
+ * What `chain.getChainInfo` reports. Omit to model a host predating chain
162
+ * discovery: the call is refused as `Unsupported`, so `getHostChainInfo`
163
+ * resolves `null` and `getChainAPI` needs an explicit environment.
164
+ */
165
+ chainInfo?: FakeChainInfo;
133
166
  }
134
167
 
135
168
  /**
@@ -143,6 +176,7 @@ export function createFakeTruApiClient(options?: CreateFakeTruApiClientOptions):
143
176
  const publicKey = toHex(options?.publicKey ?? new Uint8Array(32).fill(0x11));
144
177
  const signature = toHex(options?.signature ?? new Uint8Array(64).fill(0x22));
145
178
  const chainSupported = options?.chainSupported ?? true;
179
+ const chainInfo = options?.chainInfo;
146
180
 
147
181
  // Real in-memory KV (hex values) so getHostLocalStorage()/createLocalKvStore() round-trip.
148
182
  const kv = new Map<string, `0x${string}`>();
@@ -193,6 +227,9 @@ export function createFakeTruApiClient(options?: CreateFakeTruApiClientOptions):
193
227
  ringIndex: 0,
194
228
  ringRevision: 0,
195
229
  }),
230
+ registerRingVrfKey: () => okAsync(publicKey),
231
+ listRingVrfKeys: () => okAsync([]),
232
+ ringVrfSign: () => okAsync(signature),
196
233
  signVrf: () => okAsync({ preOutput: publicKey, proof: signature }),
197
234
  connectionStatusSubscribe: () => inertObservable(),
198
235
  },
@@ -226,7 +263,20 @@ export function createFakeTruApiClient(options?: CreateFakeTruApiClientOptions):
226
263
  return okAsync(key);
227
264
  },
228
265
  },
229
- chain: notModeled("chain"),
266
+ // Only `getChainInfo` is modeled; the rest of the domain still throws.
267
+ chain: notModeled("chain", {
268
+ getChainInfo: ({ chain }) => {
269
+ if (!chainInfo) return errAsync({ tag: "Unsupported" } as const);
270
+ const genesisHash = chainInfo.chains[chain];
271
+ if (!genesisHash) {
272
+ return errAsync({
273
+ tag: "Domain",
274
+ value: { tag: "V1", value: { tag: "NotSupported" } },
275
+ } as const);
276
+ }
277
+ return okAsync({ network: chainInfo.network, chain, genesisHash });
278
+ },
279
+ }),
230
280
  chat: notModeled("chat"),
231
281
  coinPayment: notModeled("coinPayment"),
232
282
  entropy: notModeled("entropy"),
@@ -317,6 +367,7 @@ if (import.meta.vitest) {
317
367
  "./container.js"
318
368
  );
319
369
  const { getAccountsProvider } = await import("./accounts.js");
370
+ const { getHostChainInfo } = await import("./chain-discovery.js");
320
371
  const { getPreimageManager } = await import("./truapi.js");
321
372
 
322
373
  const lookupOnce = (
@@ -360,6 +411,50 @@ if (import.meta.vitest) {
360
411
  expect(userId?.primaryUsername).toBe("carol.dot");
361
412
  });
362
413
 
414
+ test("signVrf round-trips through the real adapter", async () => {
415
+ // The fake is the only way a product can test a VRF flow: there is no
416
+ // dev-provider implementation and the e2e test host does not expose the
417
+ // call. So the fake's wire shape has to stay decodable by the adapter.
418
+ createFakeHost();
419
+ const accounts = await getAccountsProvider();
420
+ const signature = await accounts
421
+ ?.signVrf({ dotNsIdentifier: "app.dot" }, new Uint8Array([1]), [
422
+ { label: new Uint8Array([2]), value: new Uint8Array([3]) },
423
+ ])
424
+ .match(
425
+ (s) => s,
426
+ () => null,
427
+ );
428
+ expect(signature?.preOutput).toBeInstanceOf(Uint8Array);
429
+ expect(signature?.proof).toBeInstanceOf(Uint8Array);
430
+ expect(signature?.preOutput).toHaveLength(32);
431
+ expect(signature?.proof).toHaveLength(64);
432
+ });
433
+
434
+ test("chain discovery is refused by default, so a legacy host is modeled", async () => {
435
+ createFakeHost();
436
+ // Refused, not unmodeled: a bare `getChainAPI("paseo")` in a consumer
437
+ // test must not warn about the fake on every call.
438
+ expect(await getHostChainInfo(["AssetHub"])).toBeNull();
439
+ });
440
+
441
+ test("chainInfo drives discovery, with unserved roles left out", async () => {
442
+ createFakeHost({
443
+ chainInfo: { network: "paseo", chains: { AssetHub: "0xaa", Bulletin: "0xbb" } },
444
+ });
445
+ expect(await getHostChainInfo(["AssetHub", "Bulletin", "People"])).toEqual({
446
+ network: "paseo",
447
+ chains: { AssetHub: "0xaa", Bulletin: "0xbb" },
448
+ });
449
+ });
450
+
451
+ test("the rest of the chain domain still reports itself unmodeled", async () => {
452
+ const host = createFakeHost();
453
+ expect(() => (host.client.chain as { chainName?: unknown }).chainName).toThrow(
454
+ /not modeled by the fake/,
455
+ );
456
+ });
457
+
363
458
  test("statement store resolves", async () => {
364
459
  createFakeHost();
365
460
  expect(await getStatementStore()).not.toBeNull();
package/src/truapi.ts CHANGED
@@ -183,8 +183,9 @@ export async function createHostPreimageManager(): Promise<PreimageManager | nul
183
183
  // Resource-allocation / permission types, re-exported verbatim from
184
184
  // `@parity/truapi` (imported above for the local signatures):
185
185
  // - `AllocatableResource` — resource types requestable via `requestResourceAllocation`.
186
- // Since truapi 0.6.0 its `SmartContractAllowance` variant carries the tagged
187
- // `DerivationIndex` selector (`{ tag: "Left", value: number }` for a plain index).
186
+ // Its `SmartContractAllowance` variant carries the tagged `DerivationIndex`
187
+ // selector (`{ tag: "Index", value: number }` for a plain index, `{ tag:
188
+ // "Raw", value: HexString }` for a raw 32-byte index).
188
189
  // - `AllocationOutcome` — per-resource outcome, the string union
189
190
  // `"Allocated" | "Rejected" | "NotAvailable"` (RFC-10).
190
191
  // - `RemotePermission` — permission the dapp asks the host to grant via `requestPermission`.
@@ -1,31 +0,0 @@
1
- import { TrUApiClient } from '@parity/truapi';
2
-
3
- /**
4
- * Access to the in-house TruAPI client (`@parity/truapi`) for the host package.
5
- *
6
- * Environment detection and the lazily-built, cached client come from
7
- * `@parity/truapi/sandbox`; this module layers the product-sdk-specific glue on
8
- * top — an async {@link getClient} accessor and {@link subscribeWithInterrupt},
9
- * which adapts a truapi stream into the host's {@link HostSubscription} shape.
10
- *
11
- * @module
12
- */
13
-
14
- /**
15
- * Test-only seam: force {@link getClient} / {@link getClientSync} to return
16
- * `client`, and {@link isCorrectEnvironment} to report `true`. Pass `null` to
17
- * restore normal detection. Exposed through `@parity/product-sdk-host/testing`,
18
- * not the package's main entry.
19
- *
20
- * Calling this in a production build silently reroutes every host accessor to
21
- * the injected client, so we warn — it almost always means a `/testing` import
22
- * leaked into a production path.
23
- */
24
- declare function setTruApiClient(client: TrUApiClient | null): void;
25
- /**
26
- * Host-container detection. `true` when a test client is injected, otherwise the
27
- * sandbox heuristic (iframe / webview marker / injected message port).
28
- */
29
- declare function isCorrectEnvironment(): boolean;
30
-
31
- export { isCorrectEnvironment as i, setTruApiClient as s };