@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.
- package/dist/chain-discovery-nLrPzb3d.d.ts +70 -0
- package/dist/index.d.ts +102 -13
- package/dist/index.js +148 -27
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +32 -8
- package/dist/testing.js +23 -5
- package/dist/testing.js.map +1 -1
- package/package.json +4 -4
- package/src/accounts.ts +451 -33
- package/src/chain-discovery.ts +287 -0
- package/src/index.ts +12 -1
- package/src/payments.ts +1 -1
- package/src/testing.ts +106 -11
- package/src/truapi.ts +3 -2
- package/dist/transport-B0cdhwrp.d.ts +0 -31
|
@@ -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: "
|
|
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
|
-
*
|
|
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";
|
|
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>(
|
|
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"),
|
|
@@ -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
|
-
//
|
|
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`.
|
|
@@ -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 };
|