@parity/product-sdk-host 0.10.3 → 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/dist/index.d.ts +681 -462
- package/dist/index.js +890 -219
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/src/accounts.ts +544 -0
- package/src/chain-spec.ts +272 -0
- package/src/chain-transaction.ts +241 -0
- package/src/chat.ts +81 -85
- package/src/container.ts +211 -246
- package/src/entropy.ts +63 -25
- package/src/errors.ts +198 -0
- package/src/features.ts +172 -0
- package/src/index.ts +47 -22
- package/src/navigation.ts +128 -0
- package/src/notifications.ts +59 -69
- package/src/papi-provider.ts +673 -0
- package/src/payments.ts +77 -61
- package/src/permissions.ts +107 -105
- package/src/result.ts +56 -0
- package/src/theme.ts +35 -63
- package/src/transport.ts +71 -0
- package/src/truapi.ts +166 -409
- package/src/types.ts +69 -61
package/src/container.ts
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
import type { JsonRpcProvider } from "polkadot-api";
|
|
4
|
-
import {
|
|
5
|
-
import { enumValue, type Transport } from "@novasamatech/host-api";
|
|
4
|
+
import type { HexString, TrUApiClient } from "@parity/truapi";
|
|
6
5
|
|
|
6
|
+
import { formatHostError } from "./errors.js";
|
|
7
|
+
import { createHostPapiProvider } from "./papi-provider.js";
|
|
8
|
+
import { getClient, isCorrectEnvironment, subscribeWithInterrupt } from "./transport.js";
|
|
9
|
+
import { fromHex, toHex, unwrapHostResult } from "./truapi.js";
|
|
7
10
|
import type { HostLocalStorage, HostStatementStore } from "./types.js";
|
|
8
11
|
|
|
9
|
-
const
|
|
12
|
+
const textEncoder = new TextEncoder();
|
|
13
|
+
const textDecoder = new TextDecoder();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Synchronous container detection — fast heuristic check (iframe, webview
|
|
17
|
+
* marker, or injected host message port). Re-exported from the transport
|
|
18
|
+
* bootstrap, which owns the detection logic.
|
|
19
|
+
*/
|
|
20
|
+
export { isCorrectEnvironment as isInsideContainerSync } from "./transport.js";
|
|
10
21
|
|
|
11
22
|
/**
|
|
12
23
|
* Thrown by {@link getHostProvider} when the host container is reachable but does
|
|
@@ -33,35 +44,24 @@ export class ChainNotSupportedError extends Error {
|
|
|
33
44
|
}
|
|
34
45
|
|
|
35
46
|
/**
|
|
36
|
-
* Ask the host whether it can serve the given chain,
|
|
37
|
-
* `
|
|
38
|
-
*
|
|
47
|
+
* Ask the host whether it can serve the given chain, via
|
|
48
|
+
* `system.featureSupported({ tag: "Chain", … })`. Gates {@link getHostProvider}
|
|
49
|
+
* the same way the upstream wrapper's provider gated itself internally before
|
|
50
|
+
* deciding whether to start a real provider or a no-op one.
|
|
39
51
|
*
|
|
40
|
-
* @throws If the host
|
|
41
|
-
*
|
|
52
|
+
* @throws If the host rejects the support check outright — a non-hanging,
|
|
53
|
+
* catchable failure.
|
|
42
54
|
*/
|
|
43
55
|
async function isChainSupportedByHost(
|
|
44
|
-
|
|
45
|
-
genesisHash:
|
|
56
|
+
client: TrUApiClient,
|
|
57
|
+
genesisHash: HexString,
|
|
46
58
|
): Promise<boolean> {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const result = await sdk.hostApi.featureSupported(
|
|
54
|
-
enumValue("v1", enumValue("Chain", genesisHash)),
|
|
55
|
-
);
|
|
56
|
-
return result.match(
|
|
57
|
-
(ok) => ok.value === true,
|
|
58
|
-
(err) => {
|
|
59
|
-
// The reason lives at value.payload.reason for host-protocol errors and
|
|
60
|
-
// value.reason for request-level ones; tolerate both against upstream drift.
|
|
61
|
-
const value = (err as { value?: { payload?: { reason?: string }; reason?: string } })
|
|
62
|
-
?.value;
|
|
63
|
-
const reason = value?.payload?.reason ?? value?.reason ?? "unknown reason";
|
|
64
|
-
throw new Error(`Host rejected the chain-support check for ${genesisHash}: ${reason}`);
|
|
59
|
+
return client.system.featureSupported({ tag: "Chain", value: { genesisHash } }).match(
|
|
60
|
+
(response) => response.supported,
|
|
61
|
+
(error) => {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Host rejected the chain-support check for ${genesisHash}: ${formatHostError(error)}`,
|
|
64
|
+
);
|
|
65
65
|
},
|
|
66
66
|
);
|
|
67
67
|
}
|
|
@@ -71,257 +71,194 @@ async function isChainSupportedByHost(
|
|
|
71
71
|
*
|
|
72
72
|
* The SDK is designed to run exclusively inside a host container. This function
|
|
73
73
|
* is primarily useful for early validation or informational purposes.
|
|
74
|
-
*
|
|
75
|
-
* Uses product-sdk's sandboxProvider as primary detection.
|
|
76
|
-
* Falls back to manual signal checks when product-sdk is not installed.
|
|
77
74
|
*/
|
|
78
75
|
export async function isInsideContainer(): Promise<boolean> {
|
|
79
|
-
|
|
76
|
+
return isCorrectEnvironment();
|
|
77
|
+
}
|
|
80
78
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Adapt the TruAPI client's raw `localStorage` domain (hex-encoded
|
|
81
|
+
* `read`/`write`/`clear`) into the richer {@link HostLocalStorage} surface that
|
|
82
|
+
* the Storage package's `KvStore` and other consumers expect.
|
|
83
|
+
*/
|
|
84
|
+
function adaptLocalStorage(client: TrUApiClient): HostLocalStorage {
|
|
85
|
+
const ls = client.localStorage;
|
|
86
|
+
|
|
87
|
+
async function readBytes(key: string): Promise<Uint8Array | undefined> {
|
|
88
|
+
const response = await unwrapHostResult(ls.read({ key }), "host localStorage read failed");
|
|
89
|
+
return response.value !== undefined ? fromHex(response.value) : undefined;
|
|
86
90
|
}
|
|
91
|
+
|
|
92
|
+
async function writeBytes(key: string, value: Uint8Array): Promise<void> {
|
|
93
|
+
await unwrapHostResult(
|
|
94
|
+
ls.write({ key, value: toHex(value) }),
|
|
95
|
+
"host localStorage write failed",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function readString(key: string): Promise<string> {
|
|
100
|
+
const bytes = await readBytes(key);
|
|
101
|
+
return bytes ? textDecoder.decode(bytes) : "";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function writeString(key: string, value: string): Promise<void> {
|
|
105
|
+
return writeBytes(key, textEncoder.encode(value));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function readJSON(key: string): Promise<unknown> {
|
|
109
|
+
const text = await readString(key);
|
|
110
|
+
return text ? JSON.parse(text) : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function writeJSON(key: string, value: unknown): Promise<void> {
|
|
114
|
+
return writeString(key, JSON.stringify(value));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function clear(key: string): Promise<void> {
|
|
118
|
+
await unwrapHostResult(ls.clear({ key }), "host localStorage clear failed");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { readString, writeString, readJSON, writeJSON, readBytes, writeBytes, clear };
|
|
87
122
|
}
|
|
88
123
|
|
|
89
124
|
/**
|
|
90
125
|
* Get the Host API localStorage instance when running inside a container.
|
|
91
|
-
* Returns null outside a container or when
|
|
126
|
+
* Returns null outside a container or when the host transport is unavailable.
|
|
92
127
|
*/
|
|
93
128
|
export async function getHostLocalStorage(): Promise<HostLocalStorage | null> {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
const sdk = await import("@novasamatech/host-api-wrapper");
|
|
98
|
-
return sdk.hostLocalStorage as HostLocalStorage;
|
|
99
|
-
} catch (err) {
|
|
100
|
-
log.debug("getHostLocalStorage unavailable", err);
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
129
|
+
const client = await getClient();
|
|
130
|
+
return client ? adaptLocalStorage(client) : null;
|
|
103
131
|
}
|
|
104
132
|
|
|
105
133
|
/**
|
|
106
|
-
* Construct a
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* the shared singleton.
|
|
134
|
+
* Construct a host-backed `HostLocalStorage` instance. Retained for API
|
|
135
|
+
* compatibility; with the single cached TruAPI client this is equivalent to
|
|
136
|
+
* {@link getHostLocalStorage}.
|
|
110
137
|
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* @param transport - Optional transport; defaults to the sandbox transport.
|
|
114
|
-
* @returns A new `HostLocalStorage` instance, or `null` if unavailable.
|
|
138
|
+
* @returns A `HostLocalStorage` instance, or `null` if unavailable.
|
|
115
139
|
*/
|
|
116
|
-
export async function createHostLocalStorage(
|
|
117
|
-
|
|
118
|
-
): Promise<HostLocalStorage | null> {
|
|
119
|
-
if (!(await isInsideContainer())) return null;
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
const sdk = await import("@novasamatech/host-api-wrapper");
|
|
123
|
-
return sdk.createLocalStorage(transport);
|
|
124
|
-
} catch (err) {
|
|
125
|
-
log.debug("createHostLocalStorage unavailable", err);
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
140
|
+
export async function createHostLocalStorage(): Promise<HostLocalStorage | null> {
|
|
141
|
+
return getHostLocalStorage();
|
|
128
142
|
}
|
|
129
143
|
|
|
130
144
|
/**
|
|
131
145
|
* Get a PAPI-compatible JSON-RPC provider that routes through the host connection.
|
|
132
146
|
*
|
|
133
|
-
* When running inside a Polkadot container, this
|
|
134
|
-
*
|
|
135
|
-
* Returns `null` when
|
|
136
|
-
*
|
|
147
|
+
* When running inside a Polkadot container, this builds a `JsonRpcProvider` over
|
|
148
|
+
* `truApi.chain.*` (see {@link module:papi-provider}), enabling shared
|
|
149
|
+
* connections and efficient routing. Returns `null` when not running inside a
|
|
150
|
+
* container.
|
|
137
151
|
*
|
|
138
152
|
* @param genesisHash - Genesis hash of the target chain (`0x`-prefixed hex string).
|
|
139
153
|
* @returns A host-routed `JsonRpcProvider`, or `null` if unavailable.
|
|
140
154
|
* @throws {ChainNotSupportedError} When inside a container but the host can't serve
|
|
141
155
|
* the chain — surfaced instead of returning a provider that would hang forever.
|
|
142
156
|
*/
|
|
143
|
-
export async function getHostProvider(genesisHash:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
} catch (err) {
|
|
148
|
-
// Wrapper not installed — we're not running inside a container.
|
|
149
|
-
log.debug("getHostProvider unavailable", err);
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
return resolveHostProvider(sdk, genesisHash);
|
|
157
|
+
export async function getHostProvider(genesisHash: HexString): Promise<JsonRpcProvider | null> {
|
|
158
|
+
const client = await getClient();
|
|
159
|
+
if (!client) return null;
|
|
160
|
+
return resolveHostProvider(client, genesisHash);
|
|
153
161
|
}
|
|
154
162
|
|
|
155
163
|
/**
|
|
156
|
-
* Decide whether to build a host provider for `genesisHash`, given
|
|
157
|
-
*
|
|
158
|
-
* be unit-tested with a fake
|
|
159
|
-
* (browser-only) module.
|
|
164
|
+
* Decide whether to build a host provider for `genesisHash`, given a ready
|
|
165
|
+
* TruAPI client. Split out of {@link getHostProvider} so the decision logic can
|
|
166
|
+
* be unit-tested with a fake client.
|
|
160
167
|
*
|
|
161
|
-
* @returns the provider
|
|
168
|
+
* @returns the provider.
|
|
162
169
|
* @throws {ChainNotSupportedError} when the host can't serve the chain.
|
|
163
170
|
*/
|
|
164
171
|
async function resolveHostProvider(
|
|
165
|
-
|
|
166
|
-
genesisHash:
|
|
167
|
-
): Promise<JsonRpcProvider
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Inside a container: confirm the host can actually serve this chain before
|
|
176
|
-
// handing PAPI a provider. When the host doesn't support the chain, the
|
|
177
|
-
// wrapper's fallback provider silently swallows every JSON-RPC request and
|
|
178
|
-
// the caller hangs forever with no rejection. Surface a catchable error.
|
|
179
|
-
if (!(await isChainSupportedByHost(sdk, genesisHash))) {
|
|
172
|
+
client: TrUApiClient,
|
|
173
|
+
genesisHash: HexString,
|
|
174
|
+
): Promise<JsonRpcProvider> {
|
|
175
|
+
// Confirm the host can actually serve this chain before handing PAPI a
|
|
176
|
+
// provider. When the host doesn't support the chain, a provider would silently
|
|
177
|
+
// swallow every JSON-RPC request and the caller hangs forever with no
|
|
178
|
+
// rejection. Surface a catchable error instead.
|
|
179
|
+
if (!(await isChainSupportedByHost(client, genesisHash))) {
|
|
180
180
|
throw new ChainNotSupportedError(genesisHash);
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
return
|
|
183
|
+
return createHostPapiProvider(client, genesisHash);
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
/**
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// Desktop message-passing API
|
|
210
|
-
if (win.__HOST_API_PORT__ != null) return true;
|
|
211
|
-
|
|
212
|
-
return false;
|
|
186
|
+
/** Build a {@link HostStatementStore} over a TruAPI client's `statementStore` domain. */
|
|
187
|
+
function adaptStatementStore(client: TrUApiClient): HostStatementStore {
|
|
188
|
+
const ss = client.statementStore;
|
|
189
|
+
return {
|
|
190
|
+
subscribe(filter, callback) {
|
|
191
|
+
const request =
|
|
192
|
+
"matchAll" in filter
|
|
193
|
+
? ({ tag: "MatchAll", value: filter.matchAll } as const)
|
|
194
|
+
: ({ tag: "MatchAny", value: filter.matchAny } as const);
|
|
195
|
+
// `RemoteStatementStoreSubscribeItem` is structurally a StatementsPage.
|
|
196
|
+
return subscribeWithInterrupt(ss.subscribe({ request }), callback);
|
|
197
|
+
},
|
|
198
|
+
async createProofAuthorized(statement) {
|
|
199
|
+
const response = await unwrapHostResult(
|
|
200
|
+
ss.createProofAuthorized(statement),
|
|
201
|
+
"createProofAuthorized failed",
|
|
202
|
+
);
|
|
203
|
+
return response.proof;
|
|
204
|
+
},
|
|
205
|
+
async submit(signedStatement) {
|
|
206
|
+
await unwrapHostResult(ss.submit(signedStatement), "statement submit failed");
|
|
207
|
+
},
|
|
208
|
+
};
|
|
213
209
|
}
|
|
214
210
|
|
|
215
211
|
/**
|
|
216
|
-
* Get the host
|
|
212
|
+
* Get the host statement store when running inside a container, backed by
|
|
213
|
+
* `truApi.statementStore.*`.
|
|
217
214
|
*
|
|
218
|
-
* Returns a
|
|
219
|
-
*
|
|
220
|
-
* entirely. Returns `null`
|
|
215
|
+
* Returns a store with `subscribe`, `createProofAuthorized`, and `submit` that
|
|
216
|
+
* communicate through the host's native binary protocol — bypassing JSON-RPC
|
|
217
|
+
* entirely. Returns `null` outside a host container.
|
|
221
218
|
*
|
|
222
219
|
* @returns The host statement store, or `null` if unavailable.
|
|
223
220
|
*/
|
|
224
221
|
export async function getStatementStore(): Promise<HostStatementStore | null> {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return sdk.createStatementStore() as HostStatementStore;
|
|
228
|
-
} catch (err) {
|
|
229
|
-
log.debug("getStatementStore unavailable", err);
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
222
|
+
const client = await getClient();
|
|
223
|
+
return client ? adaptStatementStore(client) : null;
|
|
232
224
|
}
|
|
233
225
|
|
|
234
226
|
if (import.meta.vitest) {
|
|
235
|
-
const { test, expect, vi } = import.meta.vitest;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}) {
|
|
247
|
-
const { inContainer = true, ready = true, supported = true, featureErr = null } = opts;
|
|
227
|
+
const { test, expect, vi, afterEach } = import.meta.vitest;
|
|
228
|
+
|
|
229
|
+
afterEach(() => {
|
|
230
|
+
vi.unstubAllGlobals();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// A self-contained fake TruAPI client exposing just the `system` and `chain`
|
|
234
|
+
// domains the provider gate touches, so the chain-support decision can be
|
|
235
|
+
// tested without a real host connection.
|
|
236
|
+
function makeFakeClient(opts: { supported?: boolean; featureErr?: string | null }) {
|
|
237
|
+
const { supported = true, featureErr = null } = opts;
|
|
248
238
|
return {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
isReady: async () => ready,
|
|
252
|
-
},
|
|
253
|
-
hostApi: {
|
|
254
|
-
featureSupported: (_payload: unknown) => ({
|
|
239
|
+
system: {
|
|
240
|
+
featureSupported: (_request: unknown) => ({
|
|
255
241
|
match: (
|
|
256
|
-
okFn: (ok: {
|
|
257
|
-
errFn: (err: {
|
|
258
|
-
) =>
|
|
259
|
-
featureErr
|
|
260
|
-
? errFn({ value: { payload: { reason: featureErr } } })
|
|
261
|
-
: okFn({ tag: "v1", value: supported }),
|
|
242
|
+
okFn: (ok: { supported: boolean }) => boolean,
|
|
243
|
+
errFn: (err: { reason: string }) => boolean,
|
|
244
|
+
) => (featureErr ? errFn({ reason: featureErr }) : okFn({ supported })),
|
|
262
245
|
}),
|
|
263
246
|
},
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
},
|
|
268
|
-
} as unknown as typeof import("@novasamatech/host-api-wrapper");
|
|
247
|
+
// Read synchronously by createHostPapiProvider; never invoked here.
|
|
248
|
+
chain: {},
|
|
249
|
+
} as unknown as TrUApiClient;
|
|
269
250
|
}
|
|
270
251
|
|
|
271
|
-
test("
|
|
252
|
+
test("isInsideContainer is false in a Node environment (no window)", async () => {
|
|
272
253
|
expect(await isInsideContainer()).toBe(false);
|
|
273
254
|
});
|
|
274
255
|
|
|
275
|
-
test("
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const result = await isInsideContainer();
|
|
282
|
-
expect(result).toBe(true);
|
|
283
|
-
vi.unstubAllGlobals();
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
test("manualDetection returns true for __HOST_API_PORT__", async () => {
|
|
287
|
-
const fakeWindow = {
|
|
288
|
-
top: null,
|
|
289
|
-
__HOST_API_PORT__: 12345,
|
|
290
|
-
};
|
|
291
|
-
vi.stubGlobal("window", fakeWindow);
|
|
292
|
-
const result = await isInsideContainer();
|
|
293
|
-
expect(result).toBe(true);
|
|
294
|
-
vi.unstubAllGlobals();
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
test("manualDetection returns false when no signals present", async () => {
|
|
298
|
-
const fakeWindow = { top: null };
|
|
299
|
-
Object.defineProperty(fakeWindow, "top", { get: () => fakeWindow });
|
|
300
|
-
vi.stubGlobal("window", fakeWindow);
|
|
301
|
-
const result = await isInsideContainer();
|
|
302
|
-
expect(result).toBe(false);
|
|
303
|
-
vi.unstubAllGlobals();
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
test("manualDetection returns true for cross-origin iframe", async () => {
|
|
307
|
-
const fakeWindow = {};
|
|
308
|
-
Object.defineProperty(fakeWindow, "top", {
|
|
309
|
-
get: () => {
|
|
310
|
-
throw new DOMException("cross-origin");
|
|
311
|
-
},
|
|
312
|
-
});
|
|
313
|
-
vi.stubGlobal("window", fakeWindow);
|
|
314
|
-
const result = await isInsideContainer();
|
|
315
|
-
expect(result).toBe(true);
|
|
316
|
-
vi.unstubAllGlobals();
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
test("manualDetection returns true when window !== window.top (iframe)", async () => {
|
|
320
|
-
const fakeWindow = { top: {} }; // top is a different object
|
|
321
|
-
vi.stubGlobal("window", fakeWindow);
|
|
322
|
-
const result = await isInsideContainer();
|
|
323
|
-
expect(result).toBe(true);
|
|
324
|
-
vi.unstubAllGlobals();
|
|
256
|
+
test("isInsideContainer detects an injected host port", async () => {
|
|
257
|
+
const win = {};
|
|
258
|
+
Object.defineProperty(win, "top", { get: () => win });
|
|
259
|
+
(win as Record<string, unknown>).__HOST_API_PORT__ = 12345;
|
|
260
|
+
vi.stubGlobal("window", win);
|
|
261
|
+
expect(await isInsideContainer()).toBe(true);
|
|
325
262
|
});
|
|
326
263
|
|
|
327
264
|
test("getHostLocalStorage returns null outside container", async () => {
|
|
@@ -332,43 +269,71 @@ if (import.meta.vitest) {
|
|
|
332
269
|
expect(await createHostLocalStorage()).toBeNull();
|
|
333
270
|
});
|
|
334
271
|
|
|
335
|
-
|
|
272
|
+
test("adaptLocalStorage round-trips strings, JSON, and bytes over the TruAPI client", async () => {
|
|
273
|
+
// Minimal in-memory fake of the TruAPI localStorage domain (hex values).
|
|
274
|
+
const store = new Map<string, `0x${string}`>();
|
|
275
|
+
const okAsync = <T>(value: T) => ({
|
|
276
|
+
match: async (onOk: (v: T) => unknown) => onOk(value),
|
|
277
|
+
});
|
|
278
|
+
const fakeClient = {
|
|
279
|
+
localStorage: {
|
|
280
|
+
read: ({ key }: { key: string }) => okAsync({ value: store.get(key) }),
|
|
281
|
+
write: ({ key, value }: { key: string; value: `0x${string}` }) => {
|
|
282
|
+
store.set(key, value);
|
|
283
|
+
return okAsync(undefined);
|
|
284
|
+
},
|
|
285
|
+
clear: ({ key }: { key: string }) => {
|
|
286
|
+
store.delete(key);
|
|
287
|
+
return okAsync(undefined);
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
} as unknown as TrUApiClient;
|
|
291
|
+
|
|
292
|
+
const ls = adaptLocalStorage(fakeClient);
|
|
293
|
+
expect(await ls.readString("missing")).toBe("");
|
|
294
|
+
expect(await ls.readJSON("missing")).toBeNull();
|
|
295
|
+
expect(await ls.readBytes("missing")).toBeUndefined();
|
|
336
296
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const onCreate = (g: string) => created.push(g);
|
|
297
|
+
await ls.writeString("s", "hello");
|
|
298
|
+
expect(await ls.readString("s")).toBe("hello");
|
|
340
299
|
|
|
341
|
-
|
|
342
|
-
expect(await
|
|
343
|
-
// Outside a container -> null, without constructing a provider.
|
|
344
|
-
expect(
|
|
345
|
-
await resolveHostProvider(makeFakeSdk({ inContainer: false, onCreate }), "0xdef"),
|
|
346
|
-
).toBeNull();
|
|
300
|
+
await ls.writeJSON("j", { a: 1 });
|
|
301
|
+
expect(await ls.readJSON("j")).toEqual({ a: 1 });
|
|
347
302
|
|
|
348
|
-
|
|
303
|
+
await ls.writeBytes("b", new Uint8Array([1, 2, 3]));
|
|
304
|
+
expect(Array.from((await ls.readBytes("b")) ?? [])).toEqual([1, 2, 3]);
|
|
305
|
+
|
|
306
|
+
await ls.clear("s");
|
|
307
|
+
expect(await ls.readString("s")).toBe("");
|
|
349
308
|
});
|
|
350
309
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
await expect(resolveHostProvider(sdk, "0xabc")).rejects.toThrow();
|
|
358
|
-
// Crucially: no provider is created, so PAPI never receives a hanging no-op.
|
|
359
|
-
expect(created).toEqual([]);
|
|
310
|
+
// --- chain-support gating (resolveHostProvider over truApi.system/chain) ---
|
|
311
|
+
|
|
312
|
+
test("resolves to a provider when the host supports the chain", async () => {
|
|
313
|
+
const provider = await resolveHostProvider(makeFakeClient({ supported: true }), "0xabc");
|
|
314
|
+
// createHostPapiProvider returns a JsonRpcProvider (an onMessage -> connection fn).
|
|
315
|
+
expect(typeof provider).toBe("function");
|
|
360
316
|
});
|
|
361
317
|
|
|
362
|
-
test("
|
|
363
|
-
const err = await resolveHostProvider(
|
|
318
|
+
test("throws ChainNotSupportedError when the host doesn't support the chain", async () => {
|
|
319
|
+
const err = await resolveHostProvider(makeFakeClient({ supported: false }), "0xfeed").catch(
|
|
364
320
|
(e) => e,
|
|
365
321
|
);
|
|
366
322
|
expect(err).toBeInstanceOf(ChainNotSupportedError);
|
|
367
323
|
expect((err as ChainNotSupportedError).genesisHash).toBe("0xfeed");
|
|
368
324
|
});
|
|
369
325
|
|
|
370
|
-
test("
|
|
371
|
-
|
|
372
|
-
|
|
326
|
+
test("throws when the host rejects the support check", async () => {
|
|
327
|
+
await expect(
|
|
328
|
+
resolveHostProvider(makeFakeClient({ featureErr: "boom" }), "0xabc"),
|
|
329
|
+
).rejects.toThrow(/boom/);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("getHostProvider returns null outside a container", async () => {
|
|
333
|
+
expect(await getHostProvider("0xabc")).toBeNull();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("getStatementStore returns null outside a container", async () => {
|
|
337
|
+
expect(await getStatementStore()).toBeNull();
|
|
373
338
|
});
|
|
374
339
|
}
|