@polkadot-apps/signer 0.1.1

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.
Files changed (45) hide show
  1. package/dist/container.d.ts +11 -0
  2. package/dist/container.d.ts.map +1 -0
  3. package/dist/container.js +98 -0
  4. package/dist/container.js.map +1 -0
  5. package/dist/errors.d.ts +56 -0
  6. package/dist/errors.d.ts.map +1 -0
  7. package/dist/errors.js +226 -0
  8. package/dist/errors.js.map +1 -0
  9. package/dist/index.d.ts +15 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +14 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/providers/dev.d.ts +39 -0
  14. package/dist/providers/dev.d.ts.map +1 -0
  15. package/dist/providers/dev.js +232 -0
  16. package/dist/providers/dev.js.map +1 -0
  17. package/dist/providers/extension.d.ts +46 -0
  18. package/dist/providers/extension.d.ts.map +1 -0
  19. package/dist/providers/extension.js +363 -0
  20. package/dist/providers/extension.js.map +1 -0
  21. package/dist/providers/host.d.ts +160 -0
  22. package/dist/providers/host.d.ts.map +1 -0
  23. package/dist/providers/host.js +724 -0
  24. package/dist/providers/host.js.map +1 -0
  25. package/dist/providers/types.d.ts +45 -0
  26. package/dist/providers/types.d.ts.map +1 -0
  27. package/dist/providers/types.js +2 -0
  28. package/dist/providers/types.js.map +1 -0
  29. package/dist/retry.d.ts +23 -0
  30. package/dist/retry.d.ts.map +1 -0
  31. package/dist/retry.js +197 -0
  32. package/dist/retry.js.map +1 -0
  33. package/dist/signer-manager.d.ts +168 -0
  34. package/dist/signer-manager.d.ts.map +1 -0
  35. package/dist/signer-manager.js +1447 -0
  36. package/dist/signer-manager.js.map +1 -0
  37. package/dist/sleep.d.ts +9 -0
  38. package/dist/sleep.d.ts.map +1 -0
  39. package/dist/sleep.js +85 -0
  40. package/dist/sleep.js.map +1 -0
  41. package/dist/types.d.ts +96 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/types.js +71 -0
  44. package/dist/types.js.map +1 -0
  45. package/package.json +41 -0
@@ -0,0 +1,724 @@
1
+ import { deriveH160, ss58Encode } from "@polkadot-apps/address";
2
+ import { createLogger } from "@polkadot-apps/logger";
3
+ import { HostRejectedError, HostUnavailableError, NoAccountsError, } from "../errors.js";
4
+ import { withRetry } from "../retry.js";
5
+ import { err, ok } from "../types.js";
6
+ const log = createLogger("signer:host");
7
+ /* @integration */
8
+ async function defaultLoadSdk() {
9
+ return (await import("@novasamatech/product-sdk"));
10
+ }
11
+ /**
12
+ * Provider for the Host API (Polkadot Desktop / Android).
13
+ *
14
+ * Dynamically imports `@novasamatech/product-sdk` at runtime so it remains
15
+ * an optional peer dependency. Apps running outside a host container will
16
+ * gracefully get a `HOST_UNAVAILABLE` error.
17
+ *
18
+ * Supports both non-product accounts (user's external wallets) and product
19
+ * accounts (app-scoped derived accounts managed by the host).
20
+ */
21
+ export class HostProvider {
22
+ type = "host";
23
+ ss58Prefix;
24
+ maxRetries;
25
+ retryDelay;
26
+ loadSdk;
27
+ accountsProvider = null;
28
+ statusCleanup = null;
29
+ statusListeners = new Set();
30
+ accountListeners = new Set();
31
+ constructor(options) {
32
+ this.ss58Prefix = options?.ss58Prefix ?? 42;
33
+ this.maxRetries = options?.maxRetries ?? 3;
34
+ this.retryDelay = options?.retryDelay ?? 500;
35
+ this.loadSdk = options?.loadSdk ?? defaultLoadSdk;
36
+ }
37
+ /**
38
+ * Inject the host wallet as a Spektr extension into `window.injectedWeb3`.
39
+ *
40
+ * This is a compatibility fallback for container environments where the
41
+ * direct Host API connection fails. After injection, the host wallet
42
+ * appears as a standard browser extension and can be used via
43
+ * `ExtensionProvider`.
44
+ *
45
+ * The direct Host API path (via `HostProvider.connect()`) is preferred
46
+ * because it supports the full Host API surface (product accounts, Ring VRF,
47
+ * etc.). Spektr injection only provides non-product account access.
48
+ *
49
+ * @param loadSdk - Custom SDK loader for testing. Defaults to dynamic import.
50
+ * @returns `true` if injection succeeded, `false` otherwise.
51
+ */
52
+ static async injectSpektr(loadSdk) {
53
+ try {
54
+ const sdk = await (loadSdk ?? defaultLoadSdk)();
55
+ if (!sdk.injectSpektrExtension) {
56
+ log.warn("product-sdk does not export injectSpektrExtension");
57
+ return false;
58
+ }
59
+ const result = await sdk.injectSpektrExtension();
60
+ log.debug("Spektr injection result", { result });
61
+ return result;
62
+ }
63
+ catch (cause) {
64
+ log.warn("Spektr injection failed", { cause });
65
+ return false;
66
+ }
67
+ }
68
+ async connect(signal) {
69
+ log.debug("attempting Host API connection");
70
+ return withRetry(async () => {
71
+ if (signal?.aborted) {
72
+ return err(new HostUnavailableError("Connection aborted"));
73
+ }
74
+ return this.tryConnect();
75
+ }, {
76
+ maxAttempts: this.maxRetries,
77
+ initialDelay: this.retryDelay,
78
+ signal,
79
+ });
80
+ }
81
+ disconnect() {
82
+ if (this.statusCleanup) {
83
+ this.statusCleanup();
84
+ this.statusCleanup = null;
85
+ }
86
+ this.accountsProvider = null;
87
+ this.statusListeners.clear();
88
+ this.accountListeners.clear();
89
+ log.debug("host provider disconnected");
90
+ }
91
+ onStatusChange(callback) {
92
+ this.statusListeners.add(callback);
93
+ return () => {
94
+ this.statusListeners.delete(callback);
95
+ };
96
+ }
97
+ onAccountsChange(callback) {
98
+ this.accountListeners.add(callback);
99
+ return () => {
100
+ this.accountListeners.delete(callback);
101
+ };
102
+ }
103
+ // ── Product Account API ──────────────────────────────────────────
104
+ /**
105
+ * Get an app-scoped product account from the host.
106
+ *
107
+ * Product accounts are derived by the host wallet for each app, identified
108
+ * by `dotNsIdentifier` (e.g., "mark3t.dot"). The user controls these accounts
109
+ * but they are scoped to the requesting app.
110
+ *
111
+ * Requires a prior successful `connect()` call.
112
+ */
113
+ async getProductAccount(dotNsIdentifier, derivationIndex = 0) {
114
+ if (!this.accountsProvider) {
115
+ return err(new HostUnavailableError("Host provider is not connected"));
116
+ }
117
+ try {
118
+ const raw = (await this.accountsProvider
119
+ .getProductAccount(dotNsIdentifier, derivationIndex)
120
+ .match((account) => account, (error) => {
121
+ throw new Error(`Host rejected product account request: ${formatError(error)}`);
122
+ }));
123
+ const address = ss58Encode(raw.publicKey, this.ss58Prefix);
124
+ const productAccount = {
125
+ dotNsIdentifier,
126
+ derivationIndex,
127
+ publicKey: raw.publicKey,
128
+ };
129
+ return ok({
130
+ address,
131
+ h160Address: deriveH160(raw.publicKey),
132
+ publicKey: raw.publicKey,
133
+ name: raw.name ?? null,
134
+ source: "host",
135
+ getSigner: () => {
136
+ if (!this.accountsProvider) {
137
+ throw new Error("Host provider is disconnected");
138
+ }
139
+ return this.accountsProvider.getProductAccountSigner(productAccount);
140
+ },
141
+ });
142
+ }
143
+ catch (cause) {
144
+ log.error("failed to get product account", { cause });
145
+ return err(new HostRejectedError(cause instanceof Error ? cause.message : "Failed to get product account"));
146
+ }
147
+ }
148
+ /**
149
+ * Get a PolkadotSigner for a product account.
150
+ *
151
+ * Convenience method for when you already have the product account details.
152
+ * Requires a prior successful `connect()` call.
153
+ */
154
+ getProductAccountSigner(account) {
155
+ if (!this.accountsProvider) {
156
+ throw new Error("Host provider is not connected");
157
+ }
158
+ return this.accountsProvider.getProductAccountSigner(account);
159
+ }
160
+ /**
161
+ * Get a contextual alias for a product account via Ring VRF.
162
+ *
163
+ * Aliases prove account membership in a ring without revealing which
164
+ * account produced the alias.
165
+ *
166
+ * Requires a prior successful `connect()` call.
167
+ */
168
+ async getProductAccountAlias(dotNsIdentifier, derivationIndex = 0) {
169
+ if (!this.accountsProvider) {
170
+ return err(new HostUnavailableError("Host provider is not connected"));
171
+ }
172
+ try {
173
+ const alias = (await this.accountsProvider
174
+ .getProductAccountAlias(dotNsIdentifier, derivationIndex)
175
+ .match((result) => result, (error) => {
176
+ throw new Error(`Host rejected alias request: ${formatError(error)}`);
177
+ }));
178
+ return ok(alias);
179
+ }
180
+ catch (cause) {
181
+ log.error("failed to get product account alias", { cause });
182
+ return err(new HostRejectedError(cause instanceof Error ? cause.message : "Failed to get product account alias"));
183
+ }
184
+ }
185
+ /**
186
+ * Create a Ring VRF proof for anonymous operations.
187
+ *
188
+ * Proves that the signer is a member of the ring at the given location
189
+ * without revealing which member. Used for privacy-preserving protocols.
190
+ *
191
+ * Requires a prior successful `connect()` call.
192
+ */
193
+ async createRingVRFProof(dotNsIdentifier, derivationIndex, location, message) {
194
+ if (!this.accountsProvider) {
195
+ return err(new HostUnavailableError("Host provider is not connected"));
196
+ }
197
+ try {
198
+ const proof = (await this.accountsProvider
199
+ .createRingVRFProof(dotNsIdentifier, derivationIndex, location, message)
200
+ .match((result) => result, (error) => {
201
+ throw new Error(`Host rejected Ring VRF proof request: ${formatError(error)}`);
202
+ }));
203
+ return ok(proof);
204
+ }
205
+ catch (cause) {
206
+ log.error("failed to create Ring VRF proof", { cause });
207
+ return err(new HostRejectedError(cause instanceof Error ? cause.message : "Failed to create Ring VRF proof"));
208
+ }
209
+ }
210
+ // ── Private ──────────────────────────────────────────────────────
211
+ async tryConnect() {
212
+ // Step 1: Load product-sdk
213
+ let sdk;
214
+ try {
215
+ sdk = await this.loadSdk();
216
+ }
217
+ catch (cause) {
218
+ log.warn("product-sdk not available", { cause });
219
+ return err(new HostUnavailableError(cause instanceof Error
220
+ ? `product-sdk import failed: ${cause.message}`
221
+ : "product-sdk is not installed"));
222
+ }
223
+ // Step 2: Create accounts provider
224
+ const provider = sdk.createAccountsProvider();
225
+ this.accountsProvider = provider;
226
+ // Step 3: Fetch non-product accounts
227
+ let rawAccounts;
228
+ try {
229
+ rawAccounts = (await provider.getNonProductAccounts().match((accounts) => accounts, (error) => {
230
+ throw new Error(`Host rejected account request: ${formatError(error)}`);
231
+ }));
232
+ }
233
+ catch (cause) {
234
+ log.error("failed to get accounts from host", { cause });
235
+ return err(new HostRejectedError(cause instanceof Error ? cause.message : "Failed to get accounts from host"));
236
+ }
237
+ if (rawAccounts.length === 0) {
238
+ log.warn("host returned no accounts");
239
+ return err(new NoAccountsError("host"));
240
+ }
241
+ // Step 4: Map to SignerAccount[]
242
+ const accounts = this.mapAccounts(rawAccounts);
243
+ log.info("host connected", { accounts: accounts.length });
244
+ // Step 5: Subscribe to connection status
245
+ const sub = provider.subscribeAccountConnectionStatus((status) => {
246
+ const mapped = status === "connected" ? "connected" : "disconnected";
247
+ log.debug("host status changed", { status: mapped });
248
+ for (const listener of this.statusListeners) {
249
+ listener(mapped);
250
+ }
251
+ });
252
+ this.statusCleanup = typeof sub === "function" ? sub : () => sub.unsubscribe();
253
+ return ok(accounts);
254
+ }
255
+ mapAccounts(rawAccounts) {
256
+ return rawAccounts.map((raw) => {
257
+ const address = ss58Encode(raw.publicKey, this.ss58Prefix);
258
+ const h160Address = deriveH160(raw.publicKey);
259
+ return {
260
+ address,
261
+ h160Address,
262
+ publicKey: raw.publicKey,
263
+ name: raw.name ?? null,
264
+ source: "host",
265
+ getSigner: () => {
266
+ if (!this.accountsProvider) {
267
+ throw new Error("Host provider is disconnected");
268
+ }
269
+ return this.accountsProvider.getNonProductAccountSigner({
270
+ dotNsIdentifier: "",
271
+ derivationIndex: 0,
272
+ publicKey: raw.publicKey,
273
+ });
274
+ },
275
+ };
276
+ });
277
+ }
278
+ }
279
+ function formatError(error) {
280
+ if (error && typeof error === "object" && "tag" in error) {
281
+ return error.tag;
282
+ }
283
+ return String(error);
284
+ }
285
+ if (import.meta.vitest) {
286
+ const { test, expect, describe, vi, beforeEach } = import.meta.vitest;
287
+ function createMockProvider(options = {}) {
288
+ const accounts = options.accounts ?? [];
289
+ const shouldReject = options.shouldReject ?? false;
290
+ const mockSigner = {
291
+ publicKey: new Uint8Array(32).fill(0xbb),
292
+ };
293
+ return {
294
+ getNonProductAccounts: vi.fn().mockReturnValue({
295
+ match: async (onOk, onErr) => {
296
+ if (shouldReject) {
297
+ return onErr(options.error ?? "Unknown");
298
+ }
299
+ return onOk(accounts);
300
+ },
301
+ }),
302
+ getNonProductAccountSigner: vi.fn().mockReturnValue(mockSigner),
303
+ getProductAccount: vi.fn().mockReturnValue({
304
+ match: async (onOk, onErr) => {
305
+ if (shouldReject) {
306
+ return onErr(options.error ?? "Unknown");
307
+ }
308
+ return onOk(accounts[0] ?? { publicKey: new Uint8Array(32), name: undefined });
309
+ },
310
+ }),
311
+ getProductAccountSigner: vi.fn().mockReturnValue(mockSigner),
312
+ getProductAccountAlias: vi.fn().mockReturnValue({
313
+ match: async (onOk, onErr) => {
314
+ if (shouldReject) {
315
+ return onErr(options.error ?? "Unknown");
316
+ }
317
+ return onOk({
318
+ context: new Uint8Array(32).fill(0x01),
319
+ alias: new Uint8Array(64).fill(0x02),
320
+ });
321
+ },
322
+ }),
323
+ createRingVRFProof: vi.fn().mockReturnValue({
324
+ match: async (onOk, onErr) => {
325
+ if (shouldReject) {
326
+ return onErr(options.error ?? "Unknown");
327
+ }
328
+ return onOk(new Uint8Array(128).fill(0x03));
329
+ },
330
+ }),
331
+ subscribeAccountConnectionStatus: vi.fn().mockReturnValue(() => { }),
332
+ };
333
+ }
334
+ function createMockSdk(mockProvider) {
335
+ return { createAccountsProvider: () => mockProvider };
336
+ }
337
+ beforeEach(() => {
338
+ vi.restoreAllMocks();
339
+ });
340
+ describe("HostProvider", () => {
341
+ test("returns HOST_UNAVAILABLE when SDK load fails", async () => {
342
+ const provider = new HostProvider({
343
+ maxRetries: 1,
344
+ loadSdk: () => Promise.reject(new Error("Cannot find module")),
345
+ });
346
+ const result = await provider.connect();
347
+ expect(result.ok).toBe(false);
348
+ if (!result.ok) {
349
+ expect(result.error).toBeInstanceOf(HostUnavailableError);
350
+ expect(result.error.message).toContain("Cannot find module");
351
+ }
352
+ });
353
+ test("returns HOST_REJECTED when getNonProductAccounts fails", async () => {
354
+ const mockProvider = createMockProvider({ shouldReject: true, error: "Rejected" });
355
+ const provider = new HostProvider({
356
+ maxRetries: 1,
357
+ loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
358
+ });
359
+ const result = await provider.connect();
360
+ expect(result.ok).toBe(false);
361
+ if (!result.ok) {
362
+ expect(result.error).toBeInstanceOf(HostRejectedError);
363
+ }
364
+ });
365
+ test("returns NO_ACCOUNTS when host returns empty list", async () => {
366
+ const mockProvider = createMockProvider({ accounts: [] });
367
+ const provider = new HostProvider({
368
+ maxRetries: 1,
369
+ loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
370
+ });
371
+ const result = await provider.connect();
372
+ expect(result.ok).toBe(false);
373
+ if (!result.ok) {
374
+ expect(result.error).toBeInstanceOf(NoAccountsError);
375
+ }
376
+ });
377
+ test("maps accounts correctly on success", async () => {
378
+ const rawAccounts = [
379
+ { publicKey: new Uint8Array(32).fill(0xaa), name: "Alice" },
380
+ { publicKey: new Uint8Array(32).fill(0xbb), name: undefined },
381
+ ];
382
+ const mockProvider = createMockProvider({ accounts: rawAccounts });
383
+ const provider = new HostProvider({
384
+ maxRetries: 1,
385
+ loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
386
+ });
387
+ const result = await provider.connect();
388
+ expect(result.ok).toBe(true);
389
+ if (result.ok) {
390
+ expect(result.value).toHaveLength(2);
391
+ expect(result.value[0].name).toBe("Alice");
392
+ expect(result.value[0].source).toBe("host");
393
+ expect(result.value[0].publicKey).toEqual(rawAccounts[0].publicKey);
394
+ expect(result.value[1].name).toBeNull();
395
+ }
396
+ });
397
+ test("getSigner delegates to getNonProductAccountSigner", async () => {
398
+ const rawAccounts = [
399
+ { publicKey: new Uint8Array(32).fill(0xcc), name: "Test" },
400
+ ];
401
+ const mockProvider = createMockProvider({ accounts: rawAccounts });
402
+ const provider = new HostProvider({
403
+ maxRetries: 1,
404
+ loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
405
+ });
406
+ const result = await provider.connect();
407
+ if (result.ok) {
408
+ const signer = result.value[0].getSigner();
409
+ expect(mockProvider.getNonProductAccountSigner).toHaveBeenCalled();
410
+ expect(signer.publicKey).toEqual(new Uint8Array(32).fill(0xbb));
411
+ }
412
+ });
413
+ test("subscribeAccountConnectionStatus is wired on connect", async () => {
414
+ const rawAccounts = [{ publicKey: new Uint8Array(32).fill(0xdd) }];
415
+ const mockProvider = createMockProvider({ accounts: rawAccounts });
416
+ const provider = new HostProvider({
417
+ maxRetries: 1,
418
+ loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
419
+ });
420
+ await provider.connect();
421
+ expect(mockProvider.subscribeAccountConnectionStatus).toHaveBeenCalled();
422
+ });
423
+ test("onStatusChange emits when host status changes", async () => {
424
+ let statusCallback;
425
+ const rawAccounts = [{ publicKey: new Uint8Array(32).fill(0xee) }];
426
+ const mockProvider = createMockProvider({ accounts: rawAccounts });
427
+ mockProvider.subscribeAccountConnectionStatus.mockImplementation((cb) => {
428
+ statusCallback = cb;
429
+ return () => { };
430
+ });
431
+ const provider = new HostProvider({
432
+ maxRetries: 1,
433
+ loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
434
+ });
435
+ const statuses = [];
436
+ const unsub = provider.onStatusChange((s) => statuses.push(s));
437
+ await provider.connect();
438
+ statusCallback("disconnected");
439
+ expect(statuses).toEqual(["disconnected"]);
440
+ statusCallback("connected");
441
+ expect(statuses).toEqual(["disconnected", "connected"]);
442
+ // Unsubscribe and verify no more events
443
+ unsub();
444
+ statusCallback("disconnected");
445
+ expect(statuses).toEqual(["disconnected", "connected"]); // no change
446
+ });
447
+ test("disconnect cleans up subscriptions", async () => {
448
+ const unsubFn = vi.fn();
449
+ const rawAccounts = [{ publicKey: new Uint8Array(32).fill(0xff) }];
450
+ const mockProvider = createMockProvider({ accounts: rawAccounts });
451
+ mockProvider.subscribeAccountConnectionStatus.mockReturnValue(unsubFn);
452
+ const provider = new HostProvider({
453
+ maxRetries: 1,
454
+ loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
455
+ });
456
+ await provider.connect();
457
+ provider.disconnect();
458
+ expect(unsubFn).toHaveBeenCalled();
459
+ });
460
+ test("disconnect is idempotent", () => {
461
+ const provider = new HostProvider();
462
+ provider.disconnect();
463
+ provider.disconnect();
464
+ });
465
+ test("getSigner throws after disconnect", async () => {
466
+ const rawAccounts = [{ publicKey: new Uint8Array(32).fill(0xaa) }];
467
+ const mockProvider = createMockProvider({ accounts: rawAccounts });
468
+ const provider = new HostProvider({
469
+ maxRetries: 1,
470
+ loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
471
+ });
472
+ const result = await provider.connect();
473
+ provider.disconnect();
474
+ if (result.ok) {
475
+ expect(() => result.value[0].getSigner()).toThrow("disconnected");
476
+ }
477
+ });
478
+ test("type is 'host'", () => {
479
+ const provider = new HostProvider();
480
+ expect(provider.type).toBe("host");
481
+ });
482
+ test("AbortSignal cancels connection", async () => {
483
+ const controller = new AbortController();
484
+ controller.abort();
485
+ const provider = new HostProvider({
486
+ maxRetries: 1,
487
+ loadSdk: () => Promise.reject(new Error("Should not reach")),
488
+ });
489
+ const result = await provider.connect(controller.signal);
490
+ expect(result.ok).toBe(false);
491
+ if (!result.ok) {
492
+ expect(result.error).toBeInstanceOf(HostUnavailableError);
493
+ }
494
+ });
495
+ test("onAccountsChange adds and removes listener", () => {
496
+ const provider = new HostProvider();
497
+ const cb = () => { };
498
+ const unsub = provider.onAccountsChange(cb);
499
+ expect(typeof unsub).toBe("function");
500
+ unsub();
501
+ });
502
+ });
503
+ describe("HostProvider.injectSpektr", () => {
504
+ test("returns true when injection succeeds", async () => {
505
+ const mockSdk = {
506
+ createAccountsProvider: () => ({}),
507
+ injectSpektrExtension: () => Promise.resolve(true),
508
+ };
509
+ const result = await HostProvider.injectSpektr(() => Promise.resolve(mockSdk));
510
+ expect(result).toBe(true);
511
+ });
512
+ test("returns false when injection fails", async () => {
513
+ const mockSdk = {
514
+ createAccountsProvider: () => ({}),
515
+ injectSpektrExtension: () => Promise.resolve(false),
516
+ };
517
+ const result = await HostProvider.injectSpektr(() => Promise.resolve(mockSdk));
518
+ expect(result).toBe(false);
519
+ });
520
+ test("returns false when SDK load fails", async () => {
521
+ const result = await HostProvider.injectSpektr(() => Promise.reject(new Error("not installed")));
522
+ expect(result).toBe(false);
523
+ });
524
+ test("returns false when injectSpektrExtension not exported", async () => {
525
+ const mockSdk = {
526
+ createAccountsProvider: () => ({}),
527
+ };
528
+ const result = await HostProvider.injectSpektr(() => Promise.resolve(mockSdk));
529
+ expect(result).toBe(false);
530
+ });
531
+ });
532
+ describe("HostProvider product accounts", () => {
533
+ test("getProductAccount returns account on success", async () => {
534
+ const rawAccounts = [
535
+ { publicKey: new Uint8Array(32).fill(0xaa), name: "AppAccount" },
536
+ ];
537
+ const mockProvider = createMockProvider({ accounts: rawAccounts });
538
+ const provider = new HostProvider({
539
+ maxRetries: 1,
540
+ loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
541
+ });
542
+ await provider.connect();
543
+ const result = await provider.getProductAccount("myapp.dot", 0);
544
+ expect(result.ok).toBe(true);
545
+ if (result.ok) {
546
+ expect(result.value.source).toBe("host");
547
+ expect(mockProvider.getProductAccount).toHaveBeenCalledWith("myapp.dot", 0);
548
+ }
549
+ });
550
+ test("getProductAccount result has working getSigner", async () => {
551
+ const rawAccounts = [
552
+ { publicKey: new Uint8Array(32).fill(0xaa), name: "AppAccount" },
553
+ ];
554
+ const mockProvider = createMockProvider({ accounts: rawAccounts });
555
+ const provider = new HostProvider({
556
+ maxRetries: 1,
557
+ loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
558
+ });
559
+ await provider.connect();
560
+ const result = await provider.getProductAccount("myapp.dot", 0);
561
+ if (result.ok) {
562
+ const signer = result.value.getSigner();
563
+ expect(mockProvider.getProductAccountSigner).toHaveBeenCalled();
564
+ expect(signer.publicKey).toEqual(new Uint8Array(32).fill(0xbb));
565
+ }
566
+ });
567
+ test("getProductAccount result getSigner throws after disconnect", async () => {
568
+ const rawAccounts = [{ publicKey: new Uint8Array(32).fill(0xaa) }];
569
+ const mockProvider = createMockProvider({ accounts: rawAccounts });
570
+ const provider = new HostProvider({
571
+ maxRetries: 1,
572
+ loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
573
+ });
574
+ await provider.connect();
575
+ const result = await provider.getProductAccount("myapp.dot", 0);
576
+ provider.disconnect();
577
+ if (result.ok) {
578
+ expect(() => result.value.getSigner()).toThrow("disconnected");
579
+ }
580
+ });
581
+ test("getProductAccount returns error when not connected", async () => {
582
+ const provider = new HostProvider({ maxRetries: 1 });
583
+ const result = await provider.getProductAccount("myapp.dot");
584
+ expect(result.ok).toBe(false);
585
+ if (!result.ok) {
586
+ expect(result.error).toBeInstanceOf(HostUnavailableError);
587
+ }
588
+ });
589
+ test("getProductAccount returns error when host rejects", async () => {
590
+ const rawAccounts = [{ publicKey: new Uint8Array(32).fill(0xaa) }];
591
+ const mockProvider = createMockProvider({ accounts: rawAccounts });
592
+ mockProvider.getProductAccount.mockReturnValue({
593
+ match: async (_onOk, onErr) => onErr({ tag: "Rejected" }),
594
+ });
595
+ const provider = new HostProvider({
596
+ maxRetries: 1,
597
+ loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
598
+ });
599
+ await provider.connect();
600
+ const result = await provider.getProductAccount("myapp.dot");
601
+ expect(result.ok).toBe(false);
602
+ if (!result.ok) {
603
+ expect(result.error).toBeInstanceOf(HostRejectedError);
604
+ }
605
+ });
606
+ test("getProductAccountSigner delegates to SDK", async () => {
607
+ const rawAccounts = [{ publicKey: new Uint8Array(32).fill(0xaa) }];
608
+ const mockProvider = createMockProvider({ accounts: rawAccounts });
609
+ const provider = new HostProvider({
610
+ maxRetries: 1,
611
+ loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
612
+ });
613
+ await provider.connect();
614
+ const account = {
615
+ dotNsIdentifier: "test.dot",
616
+ derivationIndex: 0,
617
+ publicKey: new Uint8Array(32).fill(0xaa),
618
+ };
619
+ const signer = provider.getProductAccountSigner(account);
620
+ expect(mockProvider.getProductAccountSigner).toHaveBeenCalledWith(account);
621
+ expect(signer.publicKey).toEqual(new Uint8Array(32).fill(0xbb));
622
+ });
623
+ test("getProductAccountSigner throws when not connected", () => {
624
+ const provider = new HostProvider({ maxRetries: 1 });
625
+ expect(() => provider.getProductAccountSigner({
626
+ dotNsIdentifier: "test.dot",
627
+ derivationIndex: 0,
628
+ publicKey: new Uint8Array(32),
629
+ })).toThrow("not connected");
630
+ });
631
+ });
632
+ describe("HostProvider product account alias", () => {
633
+ test("getProductAccountAlias returns alias on success", async () => {
634
+ const rawAccounts = [{ publicKey: new Uint8Array(32).fill(0xaa) }];
635
+ const mockProvider = createMockProvider({ accounts: rawAccounts });
636
+ const provider = new HostProvider({
637
+ maxRetries: 1,
638
+ loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
639
+ });
640
+ await provider.connect();
641
+ const result = await provider.getProductAccountAlias("myapp.dot", 0);
642
+ expect(result.ok).toBe(true);
643
+ if (result.ok) {
644
+ expect(result.value.context).toEqual(new Uint8Array(32).fill(0x01));
645
+ expect(result.value.alias).toEqual(new Uint8Array(64).fill(0x02));
646
+ }
647
+ });
648
+ test("getProductAccountAlias returns error when not connected", async () => {
649
+ const provider = new HostProvider({ maxRetries: 1 });
650
+ const result = await provider.getProductAccountAlias("myapp.dot");
651
+ expect(result.ok).toBe(false);
652
+ if (!result.ok) {
653
+ expect(result.error).toBeInstanceOf(HostUnavailableError);
654
+ }
655
+ });
656
+ test("getProductAccountAlias returns error when host rejects", async () => {
657
+ const rawAccounts = [{ publicKey: new Uint8Array(32).fill(0xaa) }];
658
+ const mockProvider = createMockProvider({ accounts: rawAccounts });
659
+ mockProvider.getProductAccountAlias.mockReturnValue({
660
+ match: async (_onOk, onErr) => onErr({ tag: "Rejected" }),
661
+ });
662
+ const provider = new HostProvider({
663
+ maxRetries: 1,
664
+ loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
665
+ });
666
+ await provider.connect();
667
+ const result = await provider.getProductAccountAlias("myapp.dot");
668
+ expect(result.ok).toBe(false);
669
+ if (!result.ok) {
670
+ expect(result.error).toBeInstanceOf(HostRejectedError);
671
+ }
672
+ });
673
+ });
674
+ describe("HostProvider Ring VRF proof", () => {
675
+ test("createRingVRFProof returns proof on success", async () => {
676
+ const rawAccounts = [{ publicKey: new Uint8Array(32).fill(0xaa) }];
677
+ const mockProvider = createMockProvider({ accounts: rawAccounts });
678
+ const provider = new HostProvider({
679
+ maxRetries: 1,
680
+ loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
681
+ });
682
+ await provider.connect();
683
+ const location = {
684
+ genesisHash: "0x00",
685
+ ringRootHash: "0x01",
686
+ };
687
+ const result = await provider.createRingVRFProof("myapp.dot", 0, location, new Uint8Array([1, 2, 3]));
688
+ expect(result.ok).toBe(true);
689
+ if (result.ok) {
690
+ expect(result.value).toEqual(new Uint8Array(128).fill(0x03));
691
+ }
692
+ });
693
+ test("createRingVRFProof returns error when not connected", async () => {
694
+ const provider = new HostProvider({ maxRetries: 1 });
695
+ const location = {
696
+ genesisHash: "0x00",
697
+ ringRootHash: "0x01",
698
+ };
699
+ const result = await provider.createRingVRFProof("myapp.dot", 0, location, new Uint8Array([1]));
700
+ expect(result.ok).toBe(false);
701
+ if (!result.ok) {
702
+ expect(result.error).toBeInstanceOf(HostUnavailableError);
703
+ }
704
+ });
705
+ test("createRingVRFProof returns error when host rejects", async () => {
706
+ const rawAccounts = [{ publicKey: new Uint8Array(32).fill(0xaa) }];
707
+ const mockProvider = createMockProvider({ accounts: rawAccounts });
708
+ mockProvider.createRingVRFProof.mockReturnValue({
709
+ match: async (_onOk, onErr) => onErr({ tag: "Rejected" }),
710
+ });
711
+ const provider = new HostProvider({
712
+ maxRetries: 1,
713
+ loadSdk: () => Promise.resolve(createMockSdk(mockProvider)),
714
+ });
715
+ await provider.connect();
716
+ const result = await provider.createRingVRFProof("myapp.dot", 0, { genesisHash: "0x00", ringRootHash: "0x01" }, new Uint8Array([1]));
717
+ expect(result.ok).toBe(false);
718
+ if (!result.ok) {
719
+ expect(result.error).toBeInstanceOf(HostRejectedError);
720
+ }
721
+ });
722
+ });
723
+ }
724
+ //# sourceMappingURL=host.js.map