@parity/product-sdk-host 0.15.1 → 0.17.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/chain-discovery-BEvu7HaV.d.ts +107 -0
- package/dist/chunk-PAKEUP2Q.js +89 -0
- package/dist/chunk-PAKEUP2Q.js.map +1 -0
- package/dist/index.d.ts +108 -16
- package/dist/index.js +266 -56
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +39 -8
- package/dist/testing.js +26 -7
- package/dist/testing.js.map +1 -1
- package/package.json +4 -4
- package/src/accounts.ts +485 -43
- package/src/chain-discovery.ts +287 -0
- package/src/chains.ts +10 -3
- package/src/index.ts +17 -1
- package/src/papi-provider.ts +370 -37
- package/src/payments.ts +1 -1
- package/src/testing.ts +115 -13
- package/src/transport.ts +228 -4
- package/src/truapi.ts +3 -2
- package/dist/chunk-GDXSV7JV.js +0 -50
- package/dist/chunk-GDXSV7JV.js.map +0 -1
- package/dist/transport-B0cdhwrp.d.ts +0 -31
package/src/testing.ts
CHANGED
|
@@ -10,21 +10,25 @@
|
|
|
10
10
|
* makes a default `SignerManager`, `local-storage` auto-detection, and the
|
|
11
11
|
* `statement-store` / `cloud-storage` host paths testable.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* `
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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";
|
|
24
26
|
|
|
25
|
-
import {
|
|
27
|
+
import type { HostChainIdentifier } from "./chain-discovery.js";
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
import { type HostConnectionStatus, emitConnectionStatus, setTruApiClient } from "./transport.js";
|
|
30
|
+
|
|
31
|
+
export { setTruApiClient, emitConnectionStatus };
|
|
28
32
|
|
|
29
33
|
/**
|
|
30
34
|
* The public surface of a generated truapi domain client. `keyof` skips private
|
|
@@ -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>(
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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"),
|
|
@@ -254,6 +304,12 @@ type TestFinishedHook = (fn: () => void) => void;
|
|
|
254
304
|
export interface FakeHost extends Disposable {
|
|
255
305
|
/** The injected fake client (also what `getTruApi()` returns). */
|
|
256
306
|
client: TrUApiClient;
|
|
307
|
+
/**
|
|
308
|
+
* Push a status to `subscribeConnectionStatus` subscribers, so a product's
|
|
309
|
+
* reconnecting / offline UI can be exercised. `dispose()` already reports
|
|
310
|
+
* `"disconnected"`; use this to drive the states in between.
|
|
311
|
+
*/
|
|
312
|
+
emitConnectionStatus(status: HostConnectionStatus): void;
|
|
257
313
|
/** Clear the override. Idempotent; safe to call more than once. */
|
|
258
314
|
dispose(): void;
|
|
259
315
|
/** `using host = createFakeHost()` restores the real client at scope end. */
|
|
@@ -305,6 +361,7 @@ export function createFakeHost(options?: CreateFakeTruApiClientOptions): FakeHos
|
|
|
305
361
|
|
|
306
362
|
return {
|
|
307
363
|
client,
|
|
364
|
+
emitConnectionStatus,
|
|
308
365
|
dispose,
|
|
309
366
|
[Symbol.dispose]: dispose,
|
|
310
367
|
};
|
|
@@ -317,6 +374,7 @@ if (import.meta.vitest) {
|
|
|
317
374
|
"./container.js"
|
|
318
375
|
);
|
|
319
376
|
const { getAccountsProvider } = await import("./accounts.js");
|
|
377
|
+
const { getHostChainInfo } = await import("./chain-discovery.js");
|
|
320
378
|
const { getPreimageManager } = await import("./truapi.js");
|
|
321
379
|
|
|
322
380
|
const lookupOnce = (
|
|
@@ -360,6 +418,50 @@ if (import.meta.vitest) {
|
|
|
360
418
|
expect(userId?.primaryUsername).toBe("carol.dot");
|
|
361
419
|
});
|
|
362
420
|
|
|
421
|
+
test("signVrf round-trips through the real adapter", async () => {
|
|
422
|
+
// The fake is the only way a product can test a VRF flow: there is no
|
|
423
|
+
// dev-provider implementation and the e2e test host does not expose the
|
|
424
|
+
// call. So the fake's wire shape has to stay decodable by the adapter.
|
|
425
|
+
createFakeHost();
|
|
426
|
+
const accounts = await getAccountsProvider();
|
|
427
|
+
const signature = await accounts
|
|
428
|
+
?.signVrf({ dotNsIdentifier: "app.dot" }, new Uint8Array([1]), [
|
|
429
|
+
{ label: new Uint8Array([2]), value: new Uint8Array([3]) },
|
|
430
|
+
])
|
|
431
|
+
.match(
|
|
432
|
+
(s) => s,
|
|
433
|
+
() => null,
|
|
434
|
+
);
|
|
435
|
+
expect(signature?.preOutput).toBeInstanceOf(Uint8Array);
|
|
436
|
+
expect(signature?.proof).toBeInstanceOf(Uint8Array);
|
|
437
|
+
expect(signature?.preOutput).toHaveLength(32);
|
|
438
|
+
expect(signature?.proof).toHaveLength(64);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("chain discovery is refused by default, so a legacy host is modeled", async () => {
|
|
442
|
+
createFakeHost();
|
|
443
|
+
// Refused, not unmodeled: a bare `getChainAPI("paseo")` in a consumer
|
|
444
|
+
// test must not warn about the fake on every call.
|
|
445
|
+
expect(await getHostChainInfo(["AssetHub"])).toBeNull();
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("chainInfo drives discovery, with unserved roles left out", async () => {
|
|
449
|
+
createFakeHost({
|
|
450
|
+
chainInfo: { network: "paseo", chains: { AssetHub: "0xaa", Bulletin: "0xbb" } },
|
|
451
|
+
});
|
|
452
|
+
expect(await getHostChainInfo(["AssetHub", "Bulletin", "People"])).toEqual({
|
|
453
|
+
network: "paseo",
|
|
454
|
+
chains: { AssetHub: "0xaa", Bulletin: "0xbb" },
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("the rest of the chain domain still reports itself unmodeled", async () => {
|
|
459
|
+
const host = createFakeHost();
|
|
460
|
+
expect(() => (host.client.chain as { chainName?: unknown }).chainName).toThrow(
|
|
461
|
+
/not modeled by the fake/,
|
|
462
|
+
);
|
|
463
|
+
});
|
|
464
|
+
|
|
363
465
|
test("statement store resolves", async () => {
|
|
364
466
|
createFakeHost();
|
|
365
467
|
expect(await getStatementStore()).not.toBeNull();
|
package/src/transport.ts
CHANGED
|
@@ -3,18 +3,21 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Access to the in-house TruAPI client (`@parity/truapi`) for the host package.
|
|
5
5
|
*
|
|
6
|
-
* Environment detection
|
|
7
|
-
* `@parity/truapi/sandbox`; this module layers the
|
|
8
|
-
* top — an async {@link getClient} accessor
|
|
9
|
-
*
|
|
6
|
+
* Environment detection, the lazily-built cached client, and the connection-status
|
|
7
|
+
* signal come from `@parity/truapi/sandbox`; this module layers the
|
|
8
|
+
* product-sdk-specific glue on top — an async {@link getClient} accessor,
|
|
9
|
+
* {@link subscribeConnectionStatus}, and {@link subscribeWithInterrupt}, which
|
|
10
|
+
* adapts a truapi stream into the host's {@link HostSubscription} shape.
|
|
10
11
|
*
|
|
11
12
|
* @module
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
import type { ObservableLike, TrUApiClient } from "@parity/truapi";
|
|
15
16
|
import {
|
|
17
|
+
type ConnectionStatus,
|
|
16
18
|
getClientSync as sandboxGetClientSync,
|
|
17
19
|
isCorrectEnvironment as sandboxIsCorrectEnvironment,
|
|
20
|
+
subscribeConnectionStatus as sandboxSubscribeConnectionStatus,
|
|
18
21
|
} from "@parity/truapi/sandbox";
|
|
19
22
|
|
|
20
23
|
import type { HostSubscription } from "./types.js";
|
|
@@ -30,6 +33,11 @@ export interface TransportSubscription extends HostSubscription {
|
|
|
30
33
|
// no-ops there.
|
|
31
34
|
let clientOverride: TrUApiClient | null = null;
|
|
32
35
|
|
|
36
|
+
// Status subscribers registered here rather than only in the sandbox, so that
|
|
37
|
+
// flipping the test seam is an *event*. The sandbox tracks only the client it
|
|
38
|
+
// built itself, so it cannot know an injected client appeared or went away.
|
|
39
|
+
const localStatusListeners = new Set<(status: HostConnectionStatus) => void>();
|
|
40
|
+
|
|
33
41
|
function isProductionBuild(): boolean {
|
|
34
42
|
try {
|
|
35
43
|
// Must stay a plain `process.env.NODE_ENV` member expression: bundlers
|
|
@@ -51,6 +59,9 @@ function isProductionBuild(): boolean {
|
|
|
51
59
|
* Calling this in a production build silently reroutes every host accessor to
|
|
52
60
|
* the injected client, so we warn — it almost always means a `/testing` import
|
|
53
61
|
* leaked into a production path.
|
|
62
|
+
*
|
|
63
|
+
* Injecting or clearing notifies {@link subscribeConnectionStatus} subscribers,
|
|
64
|
+
* so a product's "host lost" path can be exercised by disposing the fake host.
|
|
54
65
|
*/
|
|
55
66
|
export function setTruApiClient(client: TrUApiClient | null): void {
|
|
56
67
|
if (client !== null && isProductionBuild()) {
|
|
@@ -58,7 +69,11 @@ export function setTruApiClient(client: TrUApiClient | null): void {
|
|
|
58
69
|
"[product-sdk] setTruApiClient() was called in a production build. This is a test-only seam from @parity/product-sdk-host/testing; a leaked import will silently reroute all host access to the injected client.",
|
|
59
70
|
);
|
|
60
71
|
}
|
|
72
|
+
const wasOverridden = clientOverride !== null;
|
|
61
73
|
clientOverride = client;
|
|
74
|
+
if (wasOverridden !== (client !== null)) {
|
|
75
|
+
notifyLocalStatusListeners(client !== null ? "connected" : "disconnected");
|
|
76
|
+
}
|
|
62
77
|
}
|
|
63
78
|
|
|
64
79
|
/**
|
|
@@ -85,6 +100,103 @@ export async function getClient(): Promise<TrUApiClient | null> {
|
|
|
85
100
|
return getClientSync();
|
|
86
101
|
}
|
|
87
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Connection lifecycle of the host channel: `"connecting"` while the client waits
|
|
105
|
+
* for the host, `"connected"` once the channel is established, `"disconnected"`
|
|
106
|
+
* outside a host container or after the channel closes.
|
|
107
|
+
*
|
|
108
|
+
* Not the same concept as `@parity/product-sdk-signer`'s identically-shaped
|
|
109
|
+
* `ConnectionStatus`, which tracks a signer provider rather than the transport.
|
|
110
|
+
*/
|
|
111
|
+
export type HostConnectionStatus = ConnectionStatus;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Correct one defect in the sandbox's status signal: `@parity/truapi` never clears
|
|
115
|
+
* its cached client when the pipe closes, so a subscriber arriving after a
|
|
116
|
+
* disconnect re-derives `"connecting"` from the dead client — and because the
|
|
117
|
+
* sandbox fans every change out to all listeners, that rewrites everyone's state
|
|
118
|
+
* with no way back. Hold `"disconnected"` until a real `"connected"` arrives.
|
|
119
|
+
*
|
|
120
|
+
* Applies to sandbox-sourced statuses only. A status pushed by the test seam is
|
|
121
|
+
* deliberate and passes through, so a fake host can still drive a reconnect.
|
|
122
|
+
*
|
|
123
|
+
* Outstanding upstream, not tied to the version we happen to be on: `sandbox.js`
|
|
124
|
+
* is byte-identical from 0.7.0 through 0.9.0 (npm latest) and still unfixed on
|
|
125
|
+
* `paritytech/host-rust-core` main, the repo formerly named truapi. Remove once
|
|
126
|
+
* it clears the cached client on close.
|
|
127
|
+
*/
|
|
128
|
+
function latchDisconnected(
|
|
129
|
+
previous: HostConnectionStatus | null,
|
|
130
|
+
next: HostConnectionStatus,
|
|
131
|
+
): HostConnectionStatus {
|
|
132
|
+
return next === "connecting" && previous === "disconnected" ? "disconnected" : next;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function notifyLocalStatusListeners(status: HostConnectionStatus): void {
|
|
136
|
+
// Iterate a snapshot: a listener that unsubscribes itself, or re-enters
|
|
137
|
+
// `setTruApiClient`, must not mutate the set mid-loop.
|
|
138
|
+
for (const listener of [...localStatusListeners]) listener(status);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Test-only: push `status` to every {@link subscribeConnectionStatus} subscriber,
|
|
143
|
+
* so a product can exercise its reconnecting / offline UI. The host-side
|
|
144
|
+
* counterpart of `@parity/product-sdk-signer`'s `FakeSignerProvider.emitStatus`.
|
|
145
|
+
* Exposed through `@parity/product-sdk-host/testing`, not the main entry.
|
|
146
|
+
*/
|
|
147
|
+
export function emitConnectionStatus(status: HostConnectionStatus): void {
|
|
148
|
+
if (isProductionBuild()) {
|
|
149
|
+
console.warn(
|
|
150
|
+
"[product-sdk] emitConnectionStatus() was called in a production build. This is a test-only seam from @parity/product-sdk-host/testing; a leaked import will report a fabricated connection status to real subscribers.",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
notifyLocalStatusListeners(status);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Subscribe to host-channel connection status. The callback fires synchronously
|
|
158
|
+
* with the current status and again on every change; the returned function
|
|
159
|
+
* unsubscribes. Repeats of the status you already have are suppressed.
|
|
160
|
+
*
|
|
161
|
+
* This is the **transport** channel. For the host's account-level connection —
|
|
162
|
+
* what drives `@parity/product-sdk-signer`'s `ConnectionStatus` — use
|
|
163
|
+
* `AccountsProvider.subscribeAccountConnectionStatus` instead.
|
|
164
|
+
*
|
|
165
|
+
* Subscribing is not passive: outside an established channel the first subscribe
|
|
166
|
+
* builds the client and provider, so this can be what constructs the transport.
|
|
167
|
+
*
|
|
168
|
+
* Honours the `setTruApiClient` seam — an injected client is connected by
|
|
169
|
+
* definition, and injecting or clearing one notifies live subscribers.
|
|
170
|
+
*/
|
|
171
|
+
export function subscribeConnectionStatus(
|
|
172
|
+
callback: (status: HostConnectionStatus) => void,
|
|
173
|
+
): () => void {
|
|
174
|
+
let last: HostConnectionStatus | null = null;
|
|
175
|
+
|
|
176
|
+
// One wrapped callback for both sources, so `last` stays coherent: the seam
|
|
177
|
+
// and the sandbox must not each keep their own idea of what was delivered.
|
|
178
|
+
const deliver = (status: HostConnectionStatus, fromSandbox: boolean): void => {
|
|
179
|
+
const next = fromSandbox ? latchDisconnected(last, status) : status;
|
|
180
|
+
if (next === last) return;
|
|
181
|
+
last = next;
|
|
182
|
+
callback(next);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const onLocal = (status: HostConnectionStatus) => deliver(status, false);
|
|
186
|
+
localStatusListeners.add(onLocal);
|
|
187
|
+
|
|
188
|
+
if (clientOverride !== null) {
|
|
189
|
+
onLocal("connected");
|
|
190
|
+
return () => void localStatusListeners.delete(onLocal);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const unsubscribeSandbox = sandboxSubscribeConnectionStatus((status) => deliver(status, true));
|
|
194
|
+
return () => {
|
|
195
|
+
localStatusListeners.delete(onLocal);
|
|
196
|
+
unsubscribeSandbox();
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
88
200
|
/**
|
|
89
201
|
* Adapt a truapi `ObservableLike` stream into the host's callback-style
|
|
90
202
|
* {@link HostSubscription} (`unsubscribe` + `onInterrupt`). `onNext` fires for
|
|
@@ -163,6 +275,118 @@ if (import.meta.vitest) {
|
|
|
163
275
|
}
|
|
164
276
|
});
|
|
165
277
|
|
|
278
|
+
test("subscribeConnectionStatus reports disconnected outside a container", () => {
|
|
279
|
+
const statuses: HostConnectionStatus[] = [];
|
|
280
|
+
|
|
281
|
+
const unsubscribe = subscribeConnectionStatus((status) => statuses.push(status));
|
|
282
|
+
|
|
283
|
+
expect(statuses).toEqual(["disconnected"]);
|
|
284
|
+
unsubscribe();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("subscribeConnectionStatus reports connected for an injected client", () => {
|
|
288
|
+
setTruApiClient({} as TrUApiClient);
|
|
289
|
+
const statuses: HostConnectionStatus[] = [];
|
|
290
|
+
|
|
291
|
+
const unsubscribe = subscribeConnectionStatus((status) => statuses.push(status));
|
|
292
|
+
|
|
293
|
+
// The sandbox only tracks the client it built itself, so without the
|
|
294
|
+
// override branch this would report "disconnected" while every other
|
|
295
|
+
// accessor resolved the injected client.
|
|
296
|
+
expect(statuses).toEqual(["connected"]);
|
|
297
|
+
unsubscribe();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("disposing the injected client notifies live subscribers", () => {
|
|
301
|
+
setTruApiClient({} as TrUApiClient);
|
|
302
|
+
const statuses: HostConnectionStatus[] = [];
|
|
303
|
+
const unsubscribe = subscribeConnectionStatus((status) => statuses.push(status));
|
|
304
|
+
|
|
305
|
+
setTruApiClient(null);
|
|
306
|
+
|
|
307
|
+
expect(statuses).toEqual(["connected", "disconnected"]);
|
|
308
|
+
unsubscribe();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("injecting a client notifies subscribers that started without one", () => {
|
|
312
|
+
const statuses: HostConnectionStatus[] = [];
|
|
313
|
+
const unsubscribe = subscribeConnectionStatus((status) => statuses.push(status));
|
|
314
|
+
|
|
315
|
+
setTruApiClient({} as TrUApiClient);
|
|
316
|
+
|
|
317
|
+
expect(statuses).toEqual(["disconnected", "connected"]);
|
|
318
|
+
unsubscribe();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("unsubscribe stops seam notifications", () => {
|
|
322
|
+
setTruApiClient({} as TrUApiClient);
|
|
323
|
+
const statuses: HostConnectionStatus[] = [];
|
|
324
|
+
const unsubscribe = subscribeConnectionStatus((status) => statuses.push(status));
|
|
325
|
+
|
|
326
|
+
unsubscribe();
|
|
327
|
+
setTruApiClient(null);
|
|
328
|
+
|
|
329
|
+
expect(statuses).toEqual(["connected"]);
|
|
330
|
+
expect(localStatusListeners.size).toBe(0);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("emitConnectionStatus drives transitions, including a reconnect", () => {
|
|
334
|
+
setTruApiClient({} as TrUApiClient);
|
|
335
|
+
const statuses: HostConnectionStatus[] = [];
|
|
336
|
+
const unsubscribe = subscribeConnectionStatus((status) => statuses.push(status));
|
|
337
|
+
|
|
338
|
+
emitConnectionStatus("disconnected");
|
|
339
|
+
// A seam-pushed "connecting" after "disconnected" is deliberate, so it must
|
|
340
|
+
// survive the sandbox latch — otherwise no test could drive a reconnect.
|
|
341
|
+
emitConnectionStatus("connecting");
|
|
342
|
+
emitConnectionStatus("connected");
|
|
343
|
+
|
|
344
|
+
expect(statuses).toEqual(["connected", "disconnected", "connecting", "connected"]);
|
|
345
|
+
unsubscribe();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("repeats of the current status are suppressed", () => {
|
|
349
|
+
setTruApiClient({} as TrUApiClient);
|
|
350
|
+
const statuses: HostConnectionStatus[] = [];
|
|
351
|
+
const unsubscribe = subscribeConnectionStatus((status) => statuses.push(status));
|
|
352
|
+
|
|
353
|
+
emitConnectionStatus("connected");
|
|
354
|
+
emitConnectionStatus("connected");
|
|
355
|
+
|
|
356
|
+
expect(statuses).toEqual(["connected"]);
|
|
357
|
+
unsubscribe();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("a listener that unsubscribes itself mid-notification is safe", () => {
|
|
361
|
+
setTruApiClient({} as TrUApiClient);
|
|
362
|
+
const statuses: HostConnectionStatus[] = [];
|
|
363
|
+
const handle: { unsubscribe?: () => void } = {};
|
|
364
|
+
handle.unsubscribe = subscribeConnectionStatus((status) => {
|
|
365
|
+
statuses.push(status);
|
|
366
|
+
handle.unsubscribe?.();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
setTruApiClient(null);
|
|
370
|
+
|
|
371
|
+
expect(statuses).toEqual(["connected", "disconnected"]);
|
|
372
|
+
expect(localStatusListeners.size).toBe(0);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// The sandbox latch can't be driven through the public surface — it needs a
|
|
376
|
+
// real provider close — so the correction is pinned as a pure function.
|
|
377
|
+
test("latchDisconnected holds disconnected through the stale-cache connecting", () => {
|
|
378
|
+
expect(latchDisconnected("disconnected", "connecting")).toBe("disconnected");
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("latchDisconnected passes every other transition through", () => {
|
|
382
|
+
expect(latchDisconnected(null, "disconnected")).toBe("disconnected");
|
|
383
|
+
expect(latchDisconnected(null, "connecting")).toBe("connecting");
|
|
384
|
+
expect(latchDisconnected("connecting", "connected")).toBe("connected");
|
|
385
|
+
expect(latchDisconnected("connected", "disconnected")).toBe("disconnected");
|
|
386
|
+
// A genuine reconnect still gets through once the channel re-establishes.
|
|
387
|
+
expect(latchDisconnected("connected", "connecting")).toBe("connecting");
|
|
388
|
+
});
|
|
389
|
+
|
|
166
390
|
test("subscribeWithInterrupt preserves the transport subscription id", () => {
|
|
167
391
|
const observable = {
|
|
168
392
|
subscribe: () => ({
|
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
|
-
//
|
|
187
|
-
//
|
|
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`.
|
package/dist/chunk-GDXSV7JV.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { isCorrectEnvironment as isCorrectEnvironment$1, getClientSync as getClientSync$1 } from '@parity/truapi/sandbox';
|
|
2
|
-
|
|
3
|
-
// src/transport.ts
|
|
4
|
-
var clientOverride = null;
|
|
5
|
-
function isProductionBuild() {
|
|
6
|
-
try {
|
|
7
|
-
return process.env.NODE_ENV === "production";
|
|
8
|
-
} catch {
|
|
9
|
-
return false;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
function setTruApiClient(client) {
|
|
13
|
-
if (client !== null && isProductionBuild()) {
|
|
14
|
-
console.warn(
|
|
15
|
-
"[product-sdk] setTruApiClient() was called in a production build. This is a test-only seam from @parity/product-sdk-host/testing; a leaked import will silently reroute all host access to the injected client."
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
clientOverride = client;
|
|
19
|
-
}
|
|
20
|
-
function getClientSync() {
|
|
21
|
-
return clientOverride ?? getClientSync$1();
|
|
22
|
-
}
|
|
23
|
-
function isCorrectEnvironment() {
|
|
24
|
-
return clientOverride !== null || isCorrectEnvironment$1();
|
|
25
|
-
}
|
|
26
|
-
async function getClient() {
|
|
27
|
-
return getClientSync();
|
|
28
|
-
}
|
|
29
|
-
function subscribeWithInterrupt(observable, onNext) {
|
|
30
|
-
let interruptCallback;
|
|
31
|
-
const sub = observable.subscribe({
|
|
32
|
-
next: onNext,
|
|
33
|
-
error: (reason) => interruptCallback?.(reason),
|
|
34
|
-
complete: () => interruptCallback?.()
|
|
35
|
-
});
|
|
36
|
-
return {
|
|
37
|
-
subscriptionId: sub.subscriptionId,
|
|
38
|
-
unsubscribe: () => sub.unsubscribe(),
|
|
39
|
-
onInterrupt: (callback) => {
|
|
40
|
-
interruptCallback = callback;
|
|
41
|
-
return () => {
|
|
42
|
-
if (interruptCallback === callback) interruptCallback = void 0;
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export { getClient, isCorrectEnvironment, setTruApiClient, subscribeWithInterrupt };
|
|
49
|
-
//# sourceMappingURL=chunk-GDXSV7JV.js.map
|
|
50
|
-
//# sourceMappingURL=chunk-GDXSV7JV.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/transport.ts"],"names":["sandboxGetClientSync","sandboxIsCorrectEnvironment"],"mappings":";;;AA8BA,IAAI,cAAA,GAAsC,IAAA;AAE1C,SAAS,iBAAA,GAA6B;AAClC,EAAA,IAAI;AAIA,IAAA,OAAO,OAAA,CAAQ,IAAI,QAAA,KAAa,YAAA;AAAA,EACpC,CAAA,CAAA,MAAQ;AAEJ,IAAA,OAAO,KAAA;AAAA,EACX;AACJ;AAYO,SAAS,gBAAgB,MAAA,EAAmC;AAC/D,EAAA,IAAI,MAAA,KAAW,IAAA,IAAQ,iBAAA,EAAkB,EAAG;AACxC,IAAA,OAAA,CAAQ,IAAA;AAAA,MACJ;AAAA,KACJ;AAAA,EACJ;AACA,EAAA,cAAA,GAAiB,MAAA;AACrB;AAMO,SAAS,aAAA,GAAqC;AACjD,EAAA,OAAO,kBAAkBA,eAAA,EAAqB;AAClD;AAMO,SAAS,oBAAA,GAAgC;AAC5C,EAAA,OAAO,cAAA,KAAmB,QAAQC,sBAAA,EAA4B;AAClE;AAMA,eAAsB,SAAA,GAA0C;AAC5D,EAAA,OAAO,aAAA,EAAc;AACzB;AAUO,SAAS,sBAAA,CACZ,YACA,MAAA,EACqB;AACrB,EAAA,IAAI,iBAAA;AACJ,EAAA,MAAM,GAAA,GAAM,WAAW,SAAA,CAAU;AAAA,IAC7B,IAAA,EAAM,MAAA;AAAA,IACN,KAAA,EAAO,CAAC,MAAA,KAAW,iBAAA,GAAoB,MAAM,CAAA;AAAA,IAC7C,QAAA,EAAU,MAAM,iBAAA;AAAoB,GACvC,CAAA;AACD,EAAA,OAAO;AAAA,IACH,gBAAgB,GAAA,CAAI,cAAA;AAAA,IACpB,WAAA,EAAa,MAAM,GAAA,CAAI,WAAA,EAAY;AAAA,IACnC,WAAA,EAAa,CAAC,QAAA,KAAa;AACvB,MAAA,iBAAA,GAAoB,QAAA;AACpB,MAAA,OAAO,MAAM;AACT,QAAA,IAAI,iBAAA,KAAsB,UAAU,iBAAA,GAAoB,MAAA;AAAA,MAC5D,CAAA;AAAA,IACJ;AAAA,GACJ;AACJ","file":"chunk-GDXSV7JV.js","sourcesContent":["// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/**\n * Access to the in-house TruAPI client (`@parity/truapi`) for the host package.\n *\n * Environment detection and the lazily-built, cached client come from\n * `@parity/truapi/sandbox`; this module layers the product-sdk-specific glue on\n * top — an async {@link getClient} accessor and {@link subscribeWithInterrupt},\n * which adapts a truapi stream into the host's {@link HostSubscription} shape.\n *\n * @module\n */\n\nimport type { ObservableLike, TrUApiClient } from \"@parity/truapi\";\nimport {\n getClientSync as sandboxGetClientSync,\n isCorrectEnvironment as sandboxIsCorrectEnvironment,\n} from \"@parity/truapi/sandbox\";\n\nimport type { HostSubscription } from \"./types.js\";\n\n/** A {@link HostSubscription} carrying the transport-assigned subscription id. */\nexport interface TransportSubscription extends HostSubscription {\n readonly subscriptionId: string;\n}\n\n// Test-only override. When set — via `setTruApiClient`, exposed through\n// `@parity/product-sdk-host/testing` — every host accessor resolves this client\n// instead of the sandbox one. `null` in production, so the branches below are\n// no-ops there.\nlet clientOverride: TrUApiClient | null = null;\n\nfunction isProductionBuild(): boolean {\n try {\n // Must stay a plain `process.env.NODE_ENV` member expression: bundlers\n // (Vite, esbuild, webpack) substitute it textually, which is how this\n // check works in browser builds where `process` doesn't exist.\n return process.env.NODE_ENV === \"production\";\n } catch {\n // No `process` and no bundler define — can't tell, stay quiet.\n return false;\n }\n}\n\n/**\n * Test-only seam: force {@link getClient} / {@link getClientSync} to return\n * `client`, and {@link isCorrectEnvironment} to report `true`. Pass `null` to\n * restore normal detection. Exposed through `@parity/product-sdk-host/testing`,\n * not the package's main entry.\n *\n * Calling this in a production build silently reroutes every host accessor to\n * the injected client, so we warn — it almost always means a `/testing` import\n * leaked into a production path.\n */\nexport function setTruApiClient(client: TrUApiClient | null): void {\n if (client !== null && isProductionBuild()) {\n console.warn(\n \"[product-sdk] setTruApiClient() was called in a production build. This is a test-only seam from @parity/product-sdk-host/testing; a leaked import will silently reroute all host access to the injected client.\",\n );\n }\n clientOverride = client;\n}\n\n/**\n * Synchronous TruAPI client accessor. Returns the injected test client when one\n * is set, otherwise the sandbox client (`null` outside a host container).\n */\nexport function getClientSync(): TrUApiClient | null {\n return clientOverride ?? sandboxGetClientSync();\n}\n\n/**\n * Host-container detection. `true` when a test client is injected, otherwise the\n * sandbox heuristic (iframe / webview marker / injected message port).\n */\nexport function isCorrectEnvironment(): boolean {\n return clientOverride !== null || sandboxIsCorrectEnvironment();\n}\n\n/**\n * Get the TruAPI client. Returns `null` outside a host container. Async wrapper\n * over {@link getClientSync} for the host wrappers that already `await` it.\n */\nexport async function getClient(): Promise<TrUApiClient | null> {\n return getClientSync();\n}\n\n/**\n * Adapt a truapi `ObservableLike` stream into the host's callback-style\n * {@link HostSubscription} (`unsubscribe` + `onInterrupt`). `onNext` fires for\n * each item; the registered `onInterrupt` callback fires when the host ends the\n * subscription server-side — which the generated client surfaces as either\n * `complete` (a host interrupt frame) or `error` (transport close). Shared by\n * the statement-store and preimage adapters, which both expose this shape.\n */\nexport function subscribeWithInterrupt<Item, Reason = never>(\n observable: ObservableLike<Item, Reason>,\n onNext: (item: Item) => void,\n): TransportSubscription {\n let interruptCallback: ((reason?: unknown) => void) | undefined;\n const sub = observable.subscribe({\n next: onNext,\n error: (reason) => interruptCallback?.(reason),\n complete: () => interruptCallback?.(),\n });\n return {\n subscriptionId: sub.subscriptionId,\n unsubscribe: () => sub.unsubscribe(),\n onInterrupt: (callback) => {\n interruptCallback = callback;\n return () => {\n if (interruptCallback === callback) interruptCallback = undefined;\n };\n },\n };\n}\n\nif (import.meta.vitest) {\n const { test, expect, afterEach } = import.meta.vitest;\n\n afterEach(() => setTruApiClient(null));\n\n // Environment detection and client building are covered by `@parity/truapi`'s\n // own sandbox tests; here we only assert the local glue degrades outside a\n // host container.\n test(\"getClientSync returns null outside a container\", () => {\n expect(getClientSync()).toBeNull();\n });\n\n test(\"getClient resolves null outside a container\", async () => {\n expect(await getClient()).toBeNull();\n });\n\n test(\"setTruApiClient overrides the client and container detection\", async () => {\n const fake = {} as TrUApiClient;\n setTruApiClient(fake);\n expect(getClientSync()).toBe(fake);\n expect(await getClient()).toBe(fake);\n expect(isCorrectEnvironment()).toBe(true);\n\n setTruApiClient(null);\n expect(getClientSync()).toBeNull();\n expect(isCorrectEnvironment()).toBe(false);\n });\n\n test(\"setTruApiClient warns when injecting in a production build\", () => {\n const original = process.env.NODE_ENV;\n const warnings: string[] = [];\n const realWarn = console.warn;\n console.warn = (...args: unknown[]) => void warnings.push(String(args[0]));\n try {\n process.env.NODE_ENV = \"production\";\n setTruApiClient({} as TrUApiClient);\n expect(warnings).toHaveLength(1);\n expect(warnings[0]).toContain(\"production build\");\n\n // Clearing the override must not warn.\n setTruApiClient(null);\n expect(warnings).toHaveLength(1);\n } finally {\n console.warn = realWarn;\n process.env.NODE_ENV = original;\n }\n });\n\n test(\"subscribeWithInterrupt preserves the transport subscription id\", () => {\n const observable = {\n subscribe: () => ({\n subscriptionId: \"p:17\",\n unsubscribe: () => {},\n }),\n [Symbol.observable]() {\n return this;\n },\n } as ObservableLike<never>;\n\n const subscription = subscribeWithInterrupt(observable, () => {});\n\n expect(subscription.subscriptionId).toBe(\"p:17\");\n });\n}\n"]}
|
|
@@ -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 };
|