@parity/product-sdk-signer 0.1.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.js ADDED
@@ -0,0 +1,1578 @@
1
+ // src/signer-manager.ts
2
+ import { createLogger as createLogger3 } from "@parity/product-sdk-logger";
3
+
4
+ // src/errors.ts
5
+ var SignerError = class extends Error {
6
+ constructor(message, options) {
7
+ super(message, options);
8
+ this.name = "SignerError";
9
+ }
10
+ };
11
+ var HostUnavailableError = class extends SignerError {
12
+ constructor(message = "Host API is not available") {
13
+ super(message);
14
+ this.name = "HostUnavailableError";
15
+ }
16
+ };
17
+ var HostRejectedError = class extends SignerError {
18
+ constructor(message = "Host rejected the request") {
19
+ super(message);
20
+ this.name = "HostRejectedError";
21
+ }
22
+ };
23
+ var HostDisconnectedError = class extends SignerError {
24
+ constructor(message = "Host connection lost") {
25
+ super(message);
26
+ this.name = "HostDisconnectedError";
27
+ }
28
+ };
29
+ var SigningFailedError = class extends SignerError {
30
+ constructor(cause, message) {
31
+ super(
32
+ message ?? `Signing failed: ${cause instanceof Error ? cause.message : String(cause)}`,
33
+ { cause }
34
+ );
35
+ this.name = "SigningFailedError";
36
+ }
37
+ };
38
+ var NoAccountsError = class extends SignerError {
39
+ provider;
40
+ constructor(provider, message) {
41
+ super(message ?? `No accounts available from ${provider} provider`);
42
+ this.name = "NoAccountsError";
43
+ this.provider = provider;
44
+ }
45
+ };
46
+ var TimeoutError = class extends SignerError {
47
+ operation;
48
+ ms;
49
+ constructor(operation, ms) {
50
+ super(`Operation "${operation}" timed out after ${ms}ms`);
51
+ this.name = "TimeoutError";
52
+ this.operation = operation;
53
+ this.ms = ms;
54
+ }
55
+ };
56
+ var AccountNotFoundError = class extends SignerError {
57
+ address;
58
+ constructor(address) {
59
+ super(`Account not found: ${address}`);
60
+ this.name = "AccountNotFoundError";
61
+ this.address = address;
62
+ }
63
+ };
64
+ var DestroyedError = class extends SignerError {
65
+ constructor() {
66
+ super("SignerManager has been destroyed");
67
+ this.name = "DestroyedError";
68
+ }
69
+ };
70
+ function isHostError(e) {
71
+ return e instanceof HostUnavailableError || e instanceof HostRejectedError || e instanceof HostDisconnectedError;
72
+ }
73
+ if (void 0) {
74
+ const { test, expect, describe } = void 0;
75
+ describe("error classes", () => {
76
+ test("SignerError is the base class", () => {
77
+ const e = new HostUnavailableError();
78
+ expect(e).toBeInstanceOf(SignerError);
79
+ expect(e).toBeInstanceOf(Error);
80
+ });
81
+ test("HostUnavailableError with default message", () => {
82
+ const e = new HostUnavailableError();
83
+ expect(e.name).toBe("HostUnavailableError");
84
+ expect(e.message).toBe("Host API is not available");
85
+ });
86
+ test("HostUnavailableError with custom message", () => {
87
+ const e = new HostUnavailableError("custom");
88
+ expect(e.message).toBe("custom");
89
+ });
90
+ test("HostRejectedError", () => {
91
+ const e = new HostRejectedError();
92
+ expect(e).toBeInstanceOf(SignerError);
93
+ expect(e.message).toContain("rejected");
94
+ });
95
+ test("HostDisconnectedError", () => {
96
+ const e = new HostDisconnectedError();
97
+ expect(e).toBeInstanceOf(SignerError);
98
+ expect(e.message).toContain("lost");
99
+ });
100
+ test("SigningFailedError with Error cause", () => {
101
+ const cause = new Error("bad signature");
102
+ const e = new SigningFailedError(cause);
103
+ expect(e).toBeInstanceOf(SignerError);
104
+ expect(e.cause).toBe(cause);
105
+ expect(e.message).toContain("bad signature");
106
+ });
107
+ test("SigningFailedError with string cause", () => {
108
+ const e = new SigningFailedError("oops");
109
+ expect(e.message).toContain("oops");
110
+ });
111
+ test("SigningFailedError with custom message", () => {
112
+ const e = new SigningFailedError("oops", "custom msg");
113
+ expect(e.message).toBe("custom msg");
114
+ });
115
+ test("NoAccountsError", () => {
116
+ const e = new NoAccountsError("host");
117
+ expect(e).toBeInstanceOf(SignerError);
118
+ expect(e.provider).toBe("host");
119
+ expect(e.message).toContain("host");
120
+ });
121
+ test("NoAccountsError with custom message", () => {
122
+ const e = new NoAccountsError("dev", "none found");
123
+ expect(e.message).toBe("none found");
124
+ });
125
+ test("TimeoutError", () => {
126
+ const e = new TimeoutError("connect", 5e3);
127
+ expect(e).toBeInstanceOf(SignerError);
128
+ expect(e.operation).toBe("connect");
129
+ expect(e.ms).toBe(5e3);
130
+ expect(e.message).toContain("5000");
131
+ });
132
+ test("AccountNotFoundError", () => {
133
+ const e = new AccountNotFoundError("5GrwvaEF...");
134
+ expect(e).toBeInstanceOf(SignerError);
135
+ expect(e.address).toBe("5GrwvaEF...");
136
+ });
137
+ test("DestroyedError", () => {
138
+ const e = new DestroyedError();
139
+ expect(e).toBeInstanceOf(SignerError);
140
+ expect(e.message).toContain("destroyed");
141
+ });
142
+ test("all errors have stack traces", () => {
143
+ const e = new HostUnavailableError();
144
+ expect(e.stack).toBeDefined();
145
+ expect(e.stack).toContain("HostUnavailableError");
146
+ });
147
+ });
148
+ describe("type guards", () => {
149
+ test("isHostError returns true for host errors", () => {
150
+ expect(isHostError(new HostUnavailableError())).toBe(true);
151
+ expect(isHostError(new HostRejectedError())).toBe(true);
152
+ expect(isHostError(new HostDisconnectedError())).toBe(true);
153
+ });
154
+ test("isHostError returns false for non-host errors", () => {
155
+ expect(isHostError(new SigningFailedError("x"))).toBe(false);
156
+ expect(isHostError(new NoAccountsError("dev"))).toBe(false);
157
+ expect(isHostError(new TimeoutError("op", 100))).toBe(false);
158
+ expect(isHostError(new AccountNotFoundError("x"))).toBe(false);
159
+ expect(isHostError(new DestroyedError())).toBe(false);
160
+ });
161
+ });
162
+ }
163
+
164
+ // src/signer-manager.ts
165
+ import { getHostLocalStorage } from "@parity/product-sdk-host";
166
+
167
+ // src/providers/dev.ts
168
+ import { seedToAccount } from "@parity/product-sdk-keys";
169
+ import { createLogger } from "@parity/product-sdk-logger";
170
+
171
+ // src/types.ts
172
+ function ok(value) {
173
+ return { ok: true, value };
174
+ }
175
+ function err(error) {
176
+ return { ok: false, error };
177
+ }
178
+ if (void 0) {
179
+ const { test, expect, describe } = void 0;
180
+ describe("ok", () => {
181
+ test("produces ok result with value", () => {
182
+ const result = ok(42);
183
+ expect(result.ok).toBe(true);
184
+ expect(result).toEqual({ ok: true, value: 42 });
185
+ });
186
+ test("works with complex values", () => {
187
+ const result = ok({ name: "Alice", age: 30 });
188
+ expect(result.ok).toBe(true);
189
+ if (result.ok) {
190
+ expect(result.value.name).toBe("Alice");
191
+ }
192
+ });
193
+ test("works with null value", () => {
194
+ const result = ok(null);
195
+ expect(result).toEqual({ ok: true, value: null });
196
+ });
197
+ test("works with undefined value", () => {
198
+ const result = ok(void 0);
199
+ expect(result).toEqual({ ok: true, value: void 0 });
200
+ });
201
+ });
202
+ describe("err", () => {
203
+ test("produces error result", () => {
204
+ const result = err("something went wrong");
205
+ expect(result.ok).toBe(false);
206
+ expect(result).toEqual({ ok: false, error: "something went wrong" });
207
+ });
208
+ test("works with typed error objects", () => {
209
+ const error = { type: "HOST_UNAVAILABLE", message: "no host" };
210
+ const result = err(error);
211
+ expect(result.ok).toBe(false);
212
+ if (!result.ok) {
213
+ expect(result.error.type).toBe("HOST_UNAVAILABLE");
214
+ }
215
+ });
216
+ });
217
+ describe("Result type narrowing", () => {
218
+ test("ok narrows to value access", () => {
219
+ const result = ok(42);
220
+ if (result.ok) {
221
+ const value = result.value;
222
+ expect(value).toBe(42);
223
+ } else {
224
+ expect.unreachable("should be ok");
225
+ }
226
+ });
227
+ test("err narrows to error access", () => {
228
+ const result = err("fail");
229
+ if (!result.ok) {
230
+ const error = result.error;
231
+ expect(error).toBe("fail");
232
+ } else {
233
+ expect.unreachable("should be err");
234
+ }
235
+ });
236
+ });
237
+ }
238
+
239
+ // src/providers/dev.ts
240
+ var log = createLogger("signer:dev");
241
+ var DEV_PHRASE = "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
242
+ var DEFAULT_DEV_NAMES = ["Alice", "Bob", "Charlie", "Dave", "Eve", "Ferdie"];
243
+ var DevProvider = class {
244
+ type = "dev";
245
+ names;
246
+ mnemonic;
247
+ ss58Prefix;
248
+ keyType;
249
+ constructor(options) {
250
+ this.names = options?.names ?? DEFAULT_DEV_NAMES;
251
+ this.mnemonic = options?.mnemonic ?? DEV_PHRASE;
252
+ this.ss58Prefix = options?.ss58Prefix ?? 42;
253
+ this.keyType = options?.keyType ?? "sr25519";
254
+ }
255
+ async connect() {
256
+ log.debug("creating dev accounts", { names: this.names, keyType: this.keyType });
257
+ const accounts = this.names.map((name) => {
258
+ const derived = seedToAccount(
259
+ this.mnemonic,
260
+ `//${name}`,
261
+ this.ss58Prefix,
262
+ this.keyType
263
+ );
264
+ return {
265
+ address: derived.ss58Address,
266
+ h160Address: derived.h160Address,
267
+ publicKey: derived.publicKey,
268
+ name,
269
+ source: "dev",
270
+ getSigner: () => derived.signer
271
+ };
272
+ });
273
+ log.info("dev accounts ready", { count: accounts.length });
274
+ return ok(accounts);
275
+ }
276
+ disconnect() {
277
+ }
278
+ onStatusChange(_callback) {
279
+ return () => {
280
+ };
281
+ }
282
+ onAccountsChange(_callback) {
283
+ return () => {
284
+ };
285
+ }
286
+ };
287
+ if (void 0) {
288
+ const { test, expect, describe } = void 0;
289
+ const ALICE_ADDRESS = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY";
290
+ describe("DevProvider", () => {
291
+ test("connect returns 6 accounts by default", async () => {
292
+ const provider = new DevProvider();
293
+ const result = await provider.connect();
294
+ expect(result.ok).toBe(true);
295
+ if (result.ok) {
296
+ expect(result.value).toHaveLength(6);
297
+ expect(result.value.map((a) => a.name)).toEqual([
298
+ "Alice",
299
+ "Bob",
300
+ "Charlie",
301
+ "Dave",
302
+ "Eve",
303
+ "Ferdie"
304
+ ]);
305
+ }
306
+ });
307
+ test("all accounts have source 'dev'", async () => {
308
+ const provider = new DevProvider();
309
+ const result = await provider.connect();
310
+ if (result.ok) {
311
+ for (const account of result.value) {
312
+ expect(account.source).toBe("dev");
313
+ }
314
+ }
315
+ });
316
+ test("Alice has well-known address", async () => {
317
+ const provider = new DevProvider();
318
+ const result = await provider.connect();
319
+ if (result.ok) {
320
+ expect(result.value[0].address).toBe(ALICE_ADDRESS);
321
+ }
322
+ });
323
+ test("addresses are deterministic", async () => {
324
+ const a = new DevProvider();
325
+ const b = new DevProvider();
326
+ const ra = await a.connect();
327
+ const rb = await b.connect();
328
+ if (ra.ok && rb.ok) {
329
+ expect(ra.value.map((x) => x.address)).toEqual(rb.value.map((x) => x.address));
330
+ }
331
+ });
332
+ test("each account has 32-byte publicKey", async () => {
333
+ const provider = new DevProvider();
334
+ const result = await provider.connect();
335
+ if (result.ok) {
336
+ for (const account of result.value) {
337
+ expect(account.publicKey).toBeInstanceOf(Uint8Array);
338
+ expect(account.publicKey.length).toBe(32);
339
+ }
340
+ }
341
+ });
342
+ test("getSigner returns signer with matching publicKey", async () => {
343
+ const provider = new DevProvider();
344
+ const result = await provider.connect();
345
+ if (result.ok) {
346
+ for (const account of result.value) {
347
+ const signer = account.getSigner();
348
+ expect(signer.publicKey).toEqual(account.publicKey);
349
+ }
350
+ }
351
+ });
352
+ test("custom names subset", async () => {
353
+ const provider = new DevProvider({ names: ["Alice", "Bob"] });
354
+ const result = await provider.connect();
355
+ if (result.ok) {
356
+ expect(result.value).toHaveLength(2);
357
+ expect(result.value.map((a) => a.name)).toEqual(["Alice", "Bob"]);
358
+ }
359
+ });
360
+ test("custom mnemonic produces different addresses", async () => {
361
+ const customMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
362
+ const defaultProvider = new DevProvider();
363
+ const customProvider = new DevProvider({ mnemonic: customMnemonic });
364
+ const defResult = await defaultProvider.connect();
365
+ const cusResult = await customProvider.connect();
366
+ if (defResult.ok && cusResult.ok) {
367
+ expect(defResult.value[0].address).not.toBe(cusResult.value[0].address);
368
+ }
369
+ });
370
+ test("custom ss58Prefix changes address encoding", async () => {
371
+ const generic = new DevProvider({ ss58Prefix: 42 });
372
+ const polkadot = new DevProvider({ ss58Prefix: 0 });
373
+ const rg = await generic.connect();
374
+ const rp = await polkadot.connect();
375
+ if (rg.ok && rp.ok) {
376
+ expect(rg.value[0].address).not.toBe(rp.value[0].address);
377
+ expect(rg.value[0].publicKey).toEqual(rp.value[0].publicKey);
378
+ }
379
+ });
380
+ test("disconnect is idempotent", () => {
381
+ const provider = new DevProvider();
382
+ provider.disconnect();
383
+ provider.disconnect();
384
+ });
385
+ test("onStatusChange returns no-op unsubscribe", () => {
386
+ const provider = new DevProvider();
387
+ const callback = () => {
388
+ };
389
+ const unsub = provider.onStatusChange(callback);
390
+ expect(typeof unsub).toBe("function");
391
+ unsub();
392
+ });
393
+ test("onAccountsChange returns no-op unsubscribe", () => {
394
+ const provider = new DevProvider();
395
+ const callback = () => {
396
+ };
397
+ const unsub = provider.onAccountsChange(callback);
398
+ expect(typeof unsub).toBe("function");
399
+ unsub();
400
+ });
401
+ test("type is 'dev'", () => {
402
+ const provider = new DevProvider();
403
+ expect(provider.type).toBe("dev");
404
+ });
405
+ test("empty names array returns zero accounts", async () => {
406
+ const provider = new DevProvider({ names: [] });
407
+ const result = await provider.connect();
408
+ if (result.ok) {
409
+ expect(result.value).toHaveLength(0);
410
+ }
411
+ });
412
+ test("default keyType is sr25519 (backward compatible)", async () => {
413
+ const provider = new DevProvider();
414
+ const result = await provider.connect();
415
+ if (result.ok) {
416
+ expect(result.value[0].address).toBe(ALICE_ADDRESS);
417
+ }
418
+ });
419
+ });
420
+ describe("DevProvider ed25519", () => {
421
+ test("ed25519 produces different addresses than sr25519", async () => {
422
+ const sr = new DevProvider({ keyType: "sr25519" });
423
+ const ed = new DevProvider({ keyType: "ed25519" });
424
+ const srResult = await sr.connect();
425
+ const edResult = await ed.connect();
426
+ if (srResult.ok && edResult.ok) {
427
+ expect(srResult.value[0].address).not.toBe(edResult.value[0].address);
428
+ }
429
+ });
430
+ test("ed25519 addresses are deterministic", async () => {
431
+ const a = new DevProvider({ keyType: "ed25519" });
432
+ const b = new DevProvider({ keyType: "ed25519" });
433
+ const ra = await a.connect();
434
+ const rb = await b.connect();
435
+ if (ra.ok && rb.ok) {
436
+ expect(ra.value.map((x) => x.address)).toEqual(rb.value.map((x) => x.address));
437
+ }
438
+ });
439
+ test("ed25519 getSigner has matching publicKey", async () => {
440
+ const provider = new DevProvider({ keyType: "ed25519" });
441
+ const result = await provider.connect();
442
+ if (result.ok) {
443
+ for (const account of result.value) {
444
+ const signer = account.getSigner();
445
+ expect(signer.publicKey).toEqual(account.publicKey);
446
+ }
447
+ }
448
+ });
449
+ test("ed25519 accounts have 32-byte publicKey", async () => {
450
+ const provider = new DevProvider({ keyType: "ed25519" });
451
+ const result = await provider.connect();
452
+ if (result.ok) {
453
+ for (const account of result.value) {
454
+ expect(account.publicKey).toBeInstanceOf(Uint8Array);
455
+ expect(account.publicKey.length).toBe(32);
456
+ }
457
+ }
458
+ });
459
+ });
460
+ }
461
+
462
+ // src/providers/host.ts
463
+ import { deriveH160, ss58Encode } from "@parity/product-sdk-address";
464
+ import { createLogger as createLogger2 } from "@parity/product-sdk-logger";
465
+
466
+ // src/sleep.ts
467
+ function sleep(ms, signal) {
468
+ return new Promise((resolve) => {
469
+ if (signal?.aborted) {
470
+ resolve();
471
+ return;
472
+ }
473
+ const onDone = () => {
474
+ clearTimeout(timer);
475
+ signal?.removeEventListener("abort", onDone);
476
+ resolve();
477
+ };
478
+ const timer = setTimeout(onDone, ms);
479
+ signal?.addEventListener("abort", onDone, { once: true });
480
+ });
481
+ }
482
+ if (void 0) {
483
+ const { test, expect, describe, vi, beforeEach, afterEach } = void 0;
484
+ beforeEach(() => {
485
+ vi.useFakeTimers();
486
+ });
487
+ afterEach(() => {
488
+ vi.useRealTimers();
489
+ });
490
+ describe("sleep", () => {
491
+ test("resolves after specified duration", async () => {
492
+ let resolved = false;
493
+ sleep(100).then(() => {
494
+ resolved = true;
495
+ });
496
+ expect(resolved).toBe(false);
497
+ await vi.advanceTimersByTimeAsync(99);
498
+ expect(resolved).toBe(false);
499
+ await vi.advanceTimersByTimeAsync(1);
500
+ expect(resolved).toBe(true);
501
+ });
502
+ test("resolves immediately when signal is already aborted", async () => {
503
+ const controller = new AbortController();
504
+ controller.abort();
505
+ let resolved = false;
506
+ sleep(1e4, controller.signal).then(() => {
507
+ resolved = true;
508
+ });
509
+ await vi.advanceTimersByTimeAsync(0);
510
+ expect(resolved).toBe(true);
511
+ });
512
+ test("resolves early when signal is aborted during sleep", async () => {
513
+ const controller = new AbortController();
514
+ let resolved = false;
515
+ sleep(1e4, controller.signal).then(() => {
516
+ resolved = true;
517
+ });
518
+ await vi.advanceTimersByTimeAsync(50);
519
+ expect(resolved).toBe(false);
520
+ controller.abort();
521
+ await vi.advanceTimersByTimeAsync(0);
522
+ expect(resolved).toBe(true);
523
+ });
524
+ test("works without a signal", async () => {
525
+ let resolved = false;
526
+ sleep(50).then(() => {
527
+ resolved = true;
528
+ });
529
+ await vi.advanceTimersByTimeAsync(50);
530
+ expect(resolved).toBe(true);
531
+ });
532
+ test("cleans up abort listener after natural timer expiry", async () => {
533
+ const controller = new AbortController();
534
+ const addSpy = vi.spyOn(controller.signal, "addEventListener");
535
+ const removeSpy = vi.spyOn(controller.signal, "removeEventListener");
536
+ sleep(50, controller.signal);
537
+ expect(addSpy).toHaveBeenCalledTimes(1);
538
+ await vi.advanceTimersByTimeAsync(50);
539
+ expect(removeSpy).toHaveBeenCalledTimes(1);
540
+ });
541
+ });
542
+ }
543
+
544
+ // src/retry.ts
545
+ var DEFAULT_MAX_ATTEMPTS = 3;
546
+ var DEFAULT_INITIAL_DELAY = 500;
547
+ var DEFAULT_BACKOFF_MULTIPLIER = 2;
548
+ var DEFAULT_MAX_DELAY = 1e4;
549
+ async function withRetry(fn, options) {
550
+ const maxAttempts = Math.max(1, options?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
551
+ const initialDelay = options?.initialDelay ?? DEFAULT_INITIAL_DELAY;
552
+ const backoffMultiplier = options?.backoffMultiplier ?? DEFAULT_BACKOFF_MULTIPLIER;
553
+ const maxDelay = options?.maxDelay ?? DEFAULT_MAX_DELAY;
554
+ const signal = options?.signal;
555
+ let lastResult;
556
+ let delay = initialDelay;
557
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
558
+ if (signal?.aborted && lastResult) {
559
+ return lastResult;
560
+ }
561
+ lastResult = await fn(attempt);
562
+ if (lastResult.ok) {
563
+ return lastResult;
564
+ }
565
+ if (attempt < maxAttempts - 1) {
566
+ await sleep(delay, signal);
567
+ delay = Math.min(delay * backoffMultiplier, maxDelay);
568
+ }
569
+ }
570
+ return lastResult;
571
+ }
572
+ if (void 0) {
573
+ const { test, expect, describe, vi, beforeEach, afterEach } = void 0;
574
+ const { ok: ok2, err: err2 } = await null;
575
+ beforeEach(() => {
576
+ vi.useFakeTimers();
577
+ });
578
+ afterEach(() => {
579
+ vi.useRealTimers();
580
+ });
581
+ describe("withRetry", () => {
582
+ test("succeeds on first attempt with no delay", async () => {
583
+ const fn = vi.fn().mockResolvedValue(ok2("done"));
584
+ const promise = withRetry(fn);
585
+ const result = await promise;
586
+ expect(result).toEqual(ok2("done"));
587
+ expect(fn).toHaveBeenCalledTimes(1);
588
+ expect(fn).toHaveBeenCalledWith(0);
589
+ });
590
+ test("retries on failure and succeeds on second attempt", async () => {
591
+ const fn = vi.fn().mockResolvedValueOnce(err2("fail1")).mockResolvedValueOnce(ok2("success"));
592
+ const promise = withRetry(fn, { initialDelay: 100 });
593
+ await vi.advanceTimersByTimeAsync(100);
594
+ const result = await promise;
595
+ expect(result).toEqual(ok2("success"));
596
+ expect(fn).toHaveBeenCalledTimes(2);
597
+ expect(fn).toHaveBeenNthCalledWith(1, 0);
598
+ expect(fn).toHaveBeenNthCalledWith(2, 1);
599
+ });
600
+ test("exhausts maxAttempts and returns last error", async () => {
601
+ const fn = vi.fn().mockResolvedValueOnce(err2("fail1")).mockResolvedValueOnce(err2("fail2")).mockResolvedValueOnce(err2("fail3"));
602
+ const promise = withRetry(fn, {
603
+ maxAttempts: 3,
604
+ initialDelay: 100,
605
+ backoffMultiplier: 2
606
+ });
607
+ await vi.advanceTimersByTimeAsync(100);
608
+ await vi.advanceTimersByTimeAsync(200);
609
+ const result = await promise;
610
+ expect(result).toEqual(err2("fail3"));
611
+ expect(fn).toHaveBeenCalledTimes(3);
612
+ });
613
+ test("respects AbortSignal cancellation", async () => {
614
+ const controller = new AbortController();
615
+ const fn = vi.fn().mockResolvedValue(err2("fail"));
616
+ const promise = withRetry(fn, {
617
+ maxAttempts: 5,
618
+ initialDelay: 1e3,
619
+ signal: controller.signal
620
+ });
621
+ await vi.advanceTimersByTimeAsync(0);
622
+ controller.abort();
623
+ await vi.advanceTimersByTimeAsync(0);
624
+ const result = await promise;
625
+ expect(result.ok).toBe(false);
626
+ expect(fn.mock.calls.length).toBeLessThan(5);
627
+ });
628
+ test("backoff delay increases correctly", async () => {
629
+ const fn = vi.fn().mockResolvedValueOnce(err2("e1")).mockResolvedValueOnce(err2("e2")).mockResolvedValueOnce(err2("e3")).mockResolvedValueOnce(ok2("done"));
630
+ const promise = withRetry(fn, {
631
+ maxAttempts: 4,
632
+ initialDelay: 100,
633
+ backoffMultiplier: 2
634
+ });
635
+ await vi.advanceTimersByTimeAsync(100);
636
+ await vi.advanceTimersByTimeAsync(200);
637
+ await vi.advanceTimersByTimeAsync(400);
638
+ const result = await promise;
639
+ expect(result).toEqual(ok2("done"));
640
+ expect(fn).toHaveBeenCalledTimes(4);
641
+ });
642
+ test("caps delay at maxDelay", async () => {
643
+ const fn = vi.fn().mockImplementation(async () => {
644
+ return err2("fail");
645
+ });
646
+ const promise = withRetry(fn, {
647
+ maxAttempts: 4,
648
+ initialDelay: 5e3,
649
+ backoffMultiplier: 3,
650
+ maxDelay: 8e3
651
+ });
652
+ await vi.advanceTimersByTimeAsync(5e3);
653
+ await vi.advanceTimersByTimeAsync(8e3);
654
+ await vi.advanceTimersByTimeAsync(8e3);
655
+ const result = await promise;
656
+ expect(result.ok).toBe(false);
657
+ expect(fn).toHaveBeenCalledTimes(4);
658
+ });
659
+ test("attempt number is passed correctly to fn", async () => {
660
+ const attempts = [];
661
+ const fn = vi.fn().mockImplementation(async (attempt) => {
662
+ attempts.push(attempt);
663
+ return attempt < 2 ? err2("retry") : ok2("done");
664
+ });
665
+ const promise = withRetry(fn, {
666
+ maxAttempts: 3,
667
+ initialDelay: 50
668
+ });
669
+ await vi.advanceTimersByTimeAsync(50);
670
+ await vi.advanceTimersByTimeAsync(100);
671
+ await promise;
672
+ expect(attempts).toEqual([0, 1, 2]);
673
+ });
674
+ test("single attempt with maxAttempts=1", async () => {
675
+ const fn = vi.fn().mockResolvedValue(err2("fail"));
676
+ const result = await withRetry(fn, { maxAttempts: 1 });
677
+ expect(result).toEqual(err2("fail"));
678
+ expect(fn).toHaveBeenCalledTimes(1);
679
+ });
680
+ test("signal already aborted before first attempt \u2014 fn still called once", async () => {
681
+ const controller = new AbortController();
682
+ controller.abort();
683
+ const fn = vi.fn().mockResolvedValue(err2("fail"));
684
+ const result = await withRetry(fn, {
685
+ maxAttempts: 3,
686
+ signal: controller.signal
687
+ });
688
+ expect(result.ok).toBe(false);
689
+ expect(fn).toHaveBeenCalledTimes(1);
690
+ });
691
+ });
692
+ }
693
+
694
+ // src/providers/host.ts
695
+ var log2 = createLogger2("signer:host");
696
+ async function defaultLoadSdk() {
697
+ return await import("@novasamatech/product-sdk");
698
+ }
699
+ async function defaultLoadHostApiEnum() {
700
+ return await import("@novasamatech/host-api");
701
+ }
702
+ var HostProvider = class {
703
+ type = "host";
704
+ ss58Prefix;
705
+ maxRetries;
706
+ retryDelay;
707
+ loadSdk;
708
+ loadHostApiEnum;
709
+ requestTxPermission;
710
+ accountsProvider = null;
711
+ statusCleanup = null;
712
+ statusListeners = /* @__PURE__ */ new Set();
713
+ accountListeners = /* @__PURE__ */ new Set();
714
+ constructor(options) {
715
+ this.ss58Prefix = options?.ss58Prefix ?? 42;
716
+ this.maxRetries = options?.maxRetries ?? 3;
717
+ this.retryDelay = options?.retryDelay ?? 500;
718
+ this.loadSdk = options?.loadSdk ?? defaultLoadSdk;
719
+ this.loadHostApiEnum = options?.loadHostApiEnum ?? defaultLoadHostApiEnum;
720
+ this.requestTxPermission = options?.requestTransactionSubmitPermission ?? true;
721
+ }
722
+ async connect(signal) {
723
+ log2.debug("attempting Host API connection");
724
+ return withRetry(
725
+ async () => {
726
+ if (signal?.aborted) {
727
+ return err(new HostUnavailableError("Connection aborted"));
728
+ }
729
+ return this.tryConnect();
730
+ },
731
+ {
732
+ maxAttempts: this.maxRetries,
733
+ initialDelay: this.retryDelay,
734
+ signal
735
+ }
736
+ );
737
+ }
738
+ disconnect() {
739
+ if (this.statusCleanup) {
740
+ this.statusCleanup();
741
+ this.statusCleanup = null;
742
+ }
743
+ this.accountsProvider = null;
744
+ this.statusListeners.clear();
745
+ this.accountListeners.clear();
746
+ log2.debug("host provider disconnected");
747
+ }
748
+ onStatusChange(callback) {
749
+ this.statusListeners.add(callback);
750
+ return () => {
751
+ this.statusListeners.delete(callback);
752
+ };
753
+ }
754
+ onAccountsChange(callback) {
755
+ this.accountListeners.add(callback);
756
+ return () => {
757
+ this.accountListeners.delete(callback);
758
+ };
759
+ }
760
+ // ── Product Account API ──────────────────────────────────────────
761
+ /**
762
+ * Get an app-scoped product account from the host.
763
+ *
764
+ * Product accounts are derived by the host wallet for each app, identified
765
+ * by `dotNsIdentifier` (e.g., "mark3t.dot"). The user controls these accounts
766
+ * but they are scoped to the requesting app.
767
+ *
768
+ * Requires a prior successful `connect()` call.
769
+ */
770
+ async getProductAccount(dotNsIdentifier, derivationIndex = 0) {
771
+ if (!this.accountsProvider) {
772
+ return err(new HostUnavailableError("Host provider is not connected"));
773
+ }
774
+ try {
775
+ const raw = await this.accountsProvider.getProductAccount(dotNsIdentifier, derivationIndex).match(
776
+ (account) => account,
777
+ (error) => {
778
+ throw new Error(
779
+ `Host rejected product account request: ${formatError(error)}`
780
+ );
781
+ }
782
+ );
783
+ const address = ss58Encode(raw.publicKey, this.ss58Prefix);
784
+ const productAccount = {
785
+ dotNsIdentifier,
786
+ derivationIndex,
787
+ publicKey: raw.publicKey
788
+ };
789
+ return ok({
790
+ address,
791
+ h160Address: deriveH160(raw.publicKey),
792
+ publicKey: raw.publicKey,
793
+ name: raw.name ?? null,
794
+ source: "host",
795
+ getSigner: () => {
796
+ if (!this.accountsProvider) {
797
+ throw new Error("Host provider is disconnected");
798
+ }
799
+ return this.accountsProvider.getProductAccountSigner(productAccount);
800
+ }
801
+ });
802
+ } catch (cause) {
803
+ log2.error("failed to get product account", { cause });
804
+ return err(
805
+ new HostRejectedError(
806
+ cause instanceof Error ? cause.message : "Failed to get product account"
807
+ )
808
+ );
809
+ }
810
+ }
811
+ /**
812
+ * Get a PolkadotSigner for a product account.
813
+ *
814
+ * Convenience method for when you already have the product account details.
815
+ * Requires a prior successful `connect()` call.
816
+ */
817
+ getProductAccountSigner(account) {
818
+ if (!this.accountsProvider) {
819
+ throw new Error("Host provider is not connected");
820
+ }
821
+ return this.accountsProvider.getProductAccountSigner(account);
822
+ }
823
+ /**
824
+ * Get a contextual alias for a product account via Ring VRF.
825
+ *
826
+ * Aliases prove account membership in a ring without revealing which
827
+ * account produced the alias.
828
+ *
829
+ * Requires a prior successful `connect()` call.
830
+ */
831
+ async getProductAccountAlias(dotNsIdentifier, derivationIndex = 0) {
832
+ if (!this.accountsProvider) {
833
+ return err(new HostUnavailableError("Host provider is not connected"));
834
+ }
835
+ try {
836
+ const alias = await this.accountsProvider.getProductAccountAlias(dotNsIdentifier, derivationIndex).match(
837
+ (result) => result,
838
+ (error) => {
839
+ throw new Error(`Host rejected alias request: ${formatError(error)}`);
840
+ }
841
+ );
842
+ return ok(alias);
843
+ } catch (cause) {
844
+ log2.error("failed to get product account alias", { cause });
845
+ return err(
846
+ new HostRejectedError(
847
+ cause instanceof Error ? cause.message : "Failed to get product account alias"
848
+ )
849
+ );
850
+ }
851
+ }
852
+ /**
853
+ * Create a Ring VRF proof for anonymous operations.
854
+ *
855
+ * Proves that the signer is a member of the ring at the given location
856
+ * without revealing which member. Used for privacy-preserving protocols.
857
+ *
858
+ * Requires a prior successful `connect()` call.
859
+ */
860
+ async createRingVRFProof(dotNsIdentifier, derivationIndex, location, message) {
861
+ if (!this.accountsProvider) {
862
+ return err(new HostUnavailableError("Host provider is not connected"));
863
+ }
864
+ try {
865
+ const proof = await this.accountsProvider.createRingVRFProof(dotNsIdentifier, derivationIndex, location, message).match(
866
+ (result) => result,
867
+ (error) => {
868
+ throw new Error(
869
+ `Host rejected Ring VRF proof request: ${formatError(error)}`
870
+ );
871
+ }
872
+ );
873
+ return ok(proof);
874
+ } catch (cause) {
875
+ log2.error("failed to create Ring VRF proof", { cause });
876
+ return err(
877
+ new HostRejectedError(
878
+ cause instanceof Error ? cause.message : "Failed to create Ring VRF proof"
879
+ )
880
+ );
881
+ }
882
+ }
883
+ // ── Private ──────────────────────────────────────────────────────
884
+ async tryConnect() {
885
+ let sdk;
886
+ try {
887
+ sdk = await this.loadSdk();
888
+ } catch (cause) {
889
+ log2.warn("product-sdk not available", { cause });
890
+ return err(
891
+ new HostUnavailableError(
892
+ cause instanceof Error ? `product-sdk import failed: ${cause.message}` : "product-sdk is not installed"
893
+ )
894
+ );
895
+ }
896
+ const provider = sdk.createAccountsProvider();
897
+ this.accountsProvider = provider;
898
+ let rawAccounts;
899
+ try {
900
+ rawAccounts = await provider.getNonProductAccounts().match(
901
+ (accounts2) => accounts2,
902
+ (error) => {
903
+ throw new Error(`Host rejected account request: ${formatError(error)}`);
904
+ }
905
+ );
906
+ } catch (cause) {
907
+ log2.error("failed to get accounts from host", { cause });
908
+ return err(
909
+ new HostRejectedError(
910
+ cause instanceof Error ? cause.message : "Failed to get accounts from host"
911
+ )
912
+ );
913
+ }
914
+ if (rawAccounts.length === 0) {
915
+ log2.warn("host returned no accounts");
916
+ return err(new NoAccountsError("host"));
917
+ }
918
+ if (this.requestTxPermission && sdk.hostApi) {
919
+ try {
920
+ const hostApiEnum = await this.loadHostApiEnum();
921
+ const request = hostApiEnum.enumValue("v1", {
922
+ tag: "TransactionSubmit"
923
+ });
924
+ await sdk.hostApi.permission(request).match(
925
+ () => {
926
+ log2.debug("TransactionSubmit permission granted");
927
+ },
928
+ (error) => {
929
+ log2.warn("TransactionSubmit permission rejected by host", {
930
+ error: formatError(error)
931
+ });
932
+ }
933
+ );
934
+ } catch (cause) {
935
+ log2.warn("failed to request TransactionSubmit permission", { cause });
936
+ }
937
+ }
938
+ const accounts = this.mapAccounts(rawAccounts);
939
+ log2.info("host connected", { accounts: accounts.length });
940
+ const sub = provider.subscribeAccountConnectionStatus((status) => {
941
+ const mapped = status === "connected" ? "connected" : "disconnected";
942
+ log2.debug("host status changed", { status: mapped });
943
+ for (const listener of this.statusListeners) {
944
+ listener(mapped);
945
+ }
946
+ });
947
+ this.statusCleanup = typeof sub === "function" ? sub : () => sub.unsubscribe();
948
+ return ok(accounts);
949
+ }
950
+ mapAccounts(rawAccounts) {
951
+ return rawAccounts.map((raw) => {
952
+ const address = ss58Encode(raw.publicKey, this.ss58Prefix);
953
+ const h160Address = deriveH160(raw.publicKey);
954
+ return {
955
+ address,
956
+ h160Address,
957
+ publicKey: raw.publicKey,
958
+ name: raw.name ?? null,
959
+ source: "host",
960
+ getSigner: () => {
961
+ if (!this.accountsProvider) {
962
+ throw new Error("Host provider is disconnected");
963
+ }
964
+ return this.accountsProvider.getNonProductAccountSigner({
965
+ dotNsIdentifier: "",
966
+ derivationIndex: 0,
967
+ publicKey: raw.publicKey
968
+ });
969
+ }
970
+ };
971
+ });
972
+ }
973
+ };
974
+ function formatError(error) {
975
+ if (error && typeof error === "object" && "tag" in error) {
976
+ return error.tag;
977
+ }
978
+ return String(error);
979
+ }
980
+ if (void 0) {
981
+ let createMockProvider = function(options = {}) {
982
+ const accounts = options.accounts ?? [];
983
+ const shouldReject = options.shouldReject ?? false;
984
+ const mockSigner = {
985
+ publicKey: new Uint8Array(32).fill(187)
986
+ };
987
+ return {
988
+ getNonProductAccounts: vi.fn().mockReturnValue({
989
+ match: async (onOk, onErr) => {
990
+ if (shouldReject) {
991
+ return onErr(options.error ?? "Unknown");
992
+ }
993
+ return onOk(accounts);
994
+ }
995
+ }),
996
+ getNonProductAccountSigner: vi.fn().mockReturnValue(mockSigner),
997
+ getProductAccount: vi.fn().mockReturnValue({
998
+ match: async (onOk, onErr) => {
999
+ if (shouldReject) {
1000
+ return onErr(options.error ?? "Unknown");
1001
+ }
1002
+ return onOk(accounts[0] ?? { publicKey: new Uint8Array(32), name: void 0 });
1003
+ }
1004
+ }),
1005
+ getProductAccountSigner: vi.fn().mockReturnValue(mockSigner),
1006
+ getProductAccountAlias: vi.fn().mockReturnValue({
1007
+ match: async (onOk, onErr) => {
1008
+ if (shouldReject) {
1009
+ return onErr(options.error ?? "Unknown");
1010
+ }
1011
+ return onOk({
1012
+ context: new Uint8Array(32).fill(1),
1013
+ alias: new Uint8Array(64).fill(2)
1014
+ });
1015
+ }
1016
+ }),
1017
+ createRingVRFProof: vi.fn().mockReturnValue({
1018
+ match: async (onOk, onErr) => {
1019
+ if (shouldReject) {
1020
+ return onErr(options.error ?? "Unknown");
1021
+ }
1022
+ return onOk(new Uint8Array(128).fill(3));
1023
+ }
1024
+ }),
1025
+ subscribeAccountConnectionStatus: vi.fn().mockReturnValue(() => {
1026
+ })
1027
+ };
1028
+ }, createMockSdk = function(mockProvider, opts) {
1029
+ return {
1030
+ createAccountsProvider: () => mockProvider,
1031
+ ...opts?.hostApi ? { hostApi: opts.hostApi } : {}
1032
+ };
1033
+ }, fakeResult = function(value, error) {
1034
+ return {
1035
+ match: async (onOk, onErr) => {
1036
+ if (error !== void 0) return onErr(error);
1037
+ return onOk(value);
1038
+ }
1039
+ };
1040
+ };
1041
+ createMockProvider2 = createMockProvider, createMockSdk2 = createMockSdk, fakeResult2 = fakeResult;
1042
+ const { test, expect, describe, vi, beforeEach } = void 0;
1043
+ const fakeHostApiEnum = {
1044
+ enumValue: (version, value) => ({ version, value })
1045
+ };
1046
+ beforeEach(() => {
1047
+ vi.restoreAllMocks();
1048
+ });
1049
+ describe("HostProvider", () => {
1050
+ test("returns HOST_UNAVAILABLE when SDK load fails", async () => {
1051
+ const provider = new HostProvider({
1052
+ maxRetries: 1,
1053
+ loadSdk: () => Promise.reject(new Error("Cannot find module"))
1054
+ });
1055
+ const result = await provider.connect();
1056
+ expect(result.ok).toBe(false);
1057
+ if (!result.ok) {
1058
+ expect(result.error).toBeInstanceOf(HostUnavailableError);
1059
+ expect(result.error.message).toContain("Cannot find module");
1060
+ }
1061
+ });
1062
+ test("returns HOST_REJECTED when getNonProductAccounts fails", async () => {
1063
+ const mockProvider = createMockProvider({ shouldReject: true, error: "Rejected" });
1064
+ const provider = new HostProvider({
1065
+ maxRetries: 1,
1066
+ loadSdk: () => Promise.resolve(createMockSdk(mockProvider))
1067
+ });
1068
+ const result = await provider.connect();
1069
+ expect(result.ok).toBe(false);
1070
+ if (!result.ok) {
1071
+ expect(result.error).toBeInstanceOf(HostRejectedError);
1072
+ }
1073
+ });
1074
+ test("returns NO_ACCOUNTS when host returns empty list", async () => {
1075
+ const mockProvider = createMockProvider({ accounts: [] });
1076
+ const provider = new HostProvider({
1077
+ maxRetries: 1,
1078
+ loadSdk: () => Promise.resolve(createMockSdk(mockProvider))
1079
+ });
1080
+ const result = await provider.connect();
1081
+ expect(result.ok).toBe(false);
1082
+ if (!result.ok) {
1083
+ expect(result.error).toBeInstanceOf(NoAccountsError);
1084
+ }
1085
+ });
1086
+ test("maps accounts correctly on success", async () => {
1087
+ const rawAccounts = [
1088
+ { publicKey: new Uint8Array(32).fill(170), name: "Alice" },
1089
+ { publicKey: new Uint8Array(32).fill(187), name: void 0 }
1090
+ ];
1091
+ const mockProvider = createMockProvider({ accounts: rawAccounts });
1092
+ const provider = new HostProvider({
1093
+ maxRetries: 1,
1094
+ loadSdk: () => Promise.resolve(createMockSdk(mockProvider))
1095
+ });
1096
+ const result = await provider.connect();
1097
+ expect(result.ok).toBe(true);
1098
+ if (result.ok) {
1099
+ expect(result.value).toHaveLength(2);
1100
+ expect(result.value[0].name).toBe("Alice");
1101
+ expect(result.value[0].source).toBe("host");
1102
+ expect(result.value[0].publicKey).toEqual(rawAccounts[0].publicKey);
1103
+ expect(result.value[1].name).toBeNull();
1104
+ }
1105
+ });
1106
+ test("disconnect is idempotent", () => {
1107
+ const provider = new HostProvider();
1108
+ provider.disconnect();
1109
+ provider.disconnect();
1110
+ });
1111
+ test("type is 'host'", () => {
1112
+ const provider = new HostProvider();
1113
+ expect(provider.type).toBe("host");
1114
+ });
1115
+ test("onAccountsChange adds and removes listener", () => {
1116
+ const provider = new HostProvider();
1117
+ const cb = () => {
1118
+ };
1119
+ const unsub = provider.onAccountsChange(cb);
1120
+ expect(typeof unsub).toBe("function");
1121
+ unsub();
1122
+ });
1123
+ });
1124
+ }
1125
+ var createMockProvider2;
1126
+ var createMockSdk2;
1127
+ var fakeResult2;
1128
+
1129
+ // src/signer-manager.ts
1130
+ var log3 = createLogger3("signer");
1131
+ var DEFAULT_HOST_TIMEOUT = 1e4;
1132
+ var DEFAULT_MAX_RETRIES = 3;
1133
+ var DEFAULT_SS58_PREFIX = 42;
1134
+ var DEFAULT_DAPP_NAME = "product-sdk";
1135
+ var RECONNECT_MAX_ATTEMPTS = 5;
1136
+ var RECONNECT_INITIAL_DELAY = 1e3;
1137
+ var RECONNECT_MAX_DELAY = 15e3;
1138
+ function persistenceStorageKey(dappName) {
1139
+ return `product-sdk:signer:${dappName}:selectedAccount`;
1140
+ }
1141
+ async function detectPersistence() {
1142
+ try {
1143
+ const hostStorage = await getHostLocalStorage();
1144
+ if (hostStorage) {
1145
+ log3.debug("using hostLocalStorage for persistence");
1146
+ return {
1147
+ getItem: (key) => hostStorage.readString(key),
1148
+ setItem: (key, value) => hostStorage.writeString(key, value),
1149
+ removeItem: (key) => hostStorage.writeString(key, "")
1150
+ };
1151
+ }
1152
+ } catch {
1153
+ }
1154
+ return null;
1155
+ }
1156
+ function initialState() {
1157
+ return {
1158
+ status: "disconnected",
1159
+ accounts: [],
1160
+ selectedAccount: null,
1161
+ activeProvider: null,
1162
+ error: null
1163
+ };
1164
+ }
1165
+ var SignerManager = class {
1166
+ state;
1167
+ provider = null;
1168
+ subscribers = /* @__PURE__ */ new Set();
1169
+ cleanups = [];
1170
+ isDestroyed = false;
1171
+ reconnectController = null;
1172
+ connectController = null;
1173
+ ss58Prefix;
1174
+ hostTimeout;
1175
+ maxRetries;
1176
+ providerFactory;
1177
+ dappName;
1178
+ persistenceOption;
1179
+ resolvedPersistence;
1180
+ constructor(options) {
1181
+ this.ss58Prefix = options?.ss58Prefix ?? DEFAULT_SS58_PREFIX;
1182
+ this.hostTimeout = options?.hostTimeout ?? DEFAULT_HOST_TIMEOUT;
1183
+ this.maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
1184
+ this.providerFactory = options?.createProvider;
1185
+ this.dappName = options?.dappName ?? DEFAULT_DAPP_NAME;
1186
+ this.persistenceOption = options?.persistence;
1187
+ this.resolvedPersistence = options?.persistence;
1188
+ this.state = initialState();
1189
+ }
1190
+ async getPersistence() {
1191
+ if (this.persistenceOption === null) return null;
1192
+ if (this.persistenceOption !== void 0) return this.persistenceOption;
1193
+ if (this.resolvedPersistence === void 0) {
1194
+ this.resolvedPersistence = await detectPersistence();
1195
+ }
1196
+ return this.resolvedPersistence ?? null;
1197
+ }
1198
+ /** Get a snapshot of the current state. */
1199
+ getState() {
1200
+ return this.state;
1201
+ }
1202
+ /**
1203
+ * Subscribe to state changes. The callback fires on every state mutation.
1204
+ * Returns an unsubscribe function.
1205
+ */
1206
+ subscribe(callback) {
1207
+ this.subscribers.add(callback);
1208
+ return () => {
1209
+ this.subscribers.delete(callback);
1210
+ };
1211
+ }
1212
+ /**
1213
+ * Connect to a provider.
1214
+ *
1215
+ * If no provider type is specified, connects to the Host API.
1216
+ * The SDK is designed to run exclusively inside a host container.
1217
+ *
1218
+ * When connecting to a specific provider type:
1219
+ * - `"host"`: Connect to the Host API (default, recommended)
1220
+ * - `"dev"`: Connect using dev accounts (for testing)
1221
+ */
1222
+ async connect(providerType) {
1223
+ if (this.isDestroyed) {
1224
+ return err(new DestroyedError());
1225
+ }
1226
+ this.cancelConnect();
1227
+ this.cancelReconnect();
1228
+ this.connectController = new AbortController();
1229
+ const signal = this.connectController.signal;
1230
+ this.disconnectInternal();
1231
+ this.setState({ status: "connecting", error: null });
1232
+ const targetProvider = providerType ?? "host";
1233
+ return this.connectToProvider(targetProvider, signal);
1234
+ }
1235
+ /** Disconnect from the current provider and reset state. */
1236
+ disconnect() {
1237
+ this.cancelConnect();
1238
+ this.cancelReconnect();
1239
+ this.disconnectInternal();
1240
+ this.setState(initialState());
1241
+ log3.info("disconnected");
1242
+ }
1243
+ /**
1244
+ * Select an account by address.
1245
+ * Returns the account on success, or ACCOUNT_NOT_FOUND error.
1246
+ */
1247
+ selectAccount(address) {
1248
+ if (this.isDestroyed) {
1249
+ return err(new DestroyedError());
1250
+ }
1251
+ const account = this.state.accounts.find((a) => a.address === address);
1252
+ if (!account) {
1253
+ log3.warn("account not found", { address });
1254
+ return err(new AccountNotFoundError(address));
1255
+ }
1256
+ this.setState({ selectedAccount: account });
1257
+ this.persistAccount(address);
1258
+ log3.debug("account selected", { address });
1259
+ return ok(account);
1260
+ }
1261
+ /**
1262
+ * Get the PolkadotSigner for the currently selected account.
1263
+ * Returns null if no account is selected or manager is disconnected.
1264
+ */
1265
+ getSigner() {
1266
+ return this.state.selectedAccount?.getSigner() ?? null;
1267
+ }
1268
+ /**
1269
+ * Sign arbitrary bytes with the currently selected account.
1270
+ *
1271
+ * Convenience wrapper around `PolkadotSigner.signBytes` — useful for
1272
+ * master key derivation, message signing, and proof generation without
1273
+ * constructing a full transaction.
1274
+ *
1275
+ * Returns a SIGNING_FAILED error if no account is selected or signing fails.
1276
+ */
1277
+ async signRaw(data) {
1278
+ if (this.isDestroyed) {
1279
+ return err(new DestroyedError());
1280
+ }
1281
+ const signer = this.getSigner();
1282
+ if (!signer) {
1283
+ return err(new SigningFailedError(null, "No account selected"));
1284
+ }
1285
+ try {
1286
+ const signature = await signer.signBytes(data);
1287
+ return ok(signature);
1288
+ } catch (cause) {
1289
+ log3.error("signRaw failed", { cause });
1290
+ return err(new SigningFailedError(cause));
1291
+ }
1292
+ }
1293
+ // ── Host-only: Product Account API ─────────────────────────────
1294
+ /**
1295
+ * Get an app-scoped product account from the host.
1296
+ *
1297
+ * Product accounts are derived by the host wallet for each app, identified
1298
+ * by `dotNsIdentifier` (e.g., "mark3t.dot"). Only available when connected
1299
+ * via the host provider — returns HOST_UNAVAILABLE otherwise.
1300
+ *
1301
+ * @example
1302
+ * ```ts
1303
+ * const result = await manager.getProductAccount("myapp.dot");
1304
+ * if (result.ok) {
1305
+ * const signer = result.value.getSigner();
1306
+ * }
1307
+ * ```
1308
+ */
1309
+ async getProductAccount(dotNsIdentifier, derivationIndex = 0) {
1310
+ if (this.isDestroyed) return err(new DestroyedError());
1311
+ const host = this.getHostProvider();
1312
+ if (!host) {
1313
+ return err(
1314
+ new HostUnavailableError("Product accounts require a host provider connection")
1315
+ );
1316
+ }
1317
+ return host.getProductAccount(dotNsIdentifier, derivationIndex);
1318
+ }
1319
+ /**
1320
+ * Get a contextual alias for a product account via Ring VRF.
1321
+ *
1322
+ * Aliases prove account membership in a ring without revealing which
1323
+ * account produced the alias. Only available when connected via the host
1324
+ * provider — returns HOST_UNAVAILABLE otherwise.
1325
+ */
1326
+ async getProductAccountAlias(dotNsIdentifier, derivationIndex = 0) {
1327
+ if (this.isDestroyed) return err(new DestroyedError());
1328
+ const host = this.getHostProvider();
1329
+ if (!host) {
1330
+ return err(
1331
+ new HostUnavailableError(
1332
+ "Product account aliases require a host provider connection"
1333
+ )
1334
+ );
1335
+ }
1336
+ return host.getProductAccountAlias(dotNsIdentifier, derivationIndex);
1337
+ }
1338
+ /**
1339
+ * Create a Ring VRF proof for anonymous operations.
1340
+ *
1341
+ * Proves that the signer is a member of the ring at the given location
1342
+ * without revealing which member. Only available when connected via the
1343
+ * host provider — returns HOST_UNAVAILABLE otherwise.
1344
+ */
1345
+ async createRingVRFProof(dotNsIdentifier, derivationIndex, location, message) {
1346
+ if (this.isDestroyed) return err(new DestroyedError());
1347
+ const host = this.getHostProvider();
1348
+ if (!host) {
1349
+ return err(
1350
+ new HostUnavailableError("Ring VRF proofs require a host provider connection")
1351
+ );
1352
+ }
1353
+ return host.createRingVRFProof(dotNsIdentifier, derivationIndex, location, message);
1354
+ }
1355
+ /**
1356
+ * Destroy the manager and release all resources.
1357
+ * After calling destroy(), the manager is unusable.
1358
+ */
1359
+ destroy() {
1360
+ if (this.isDestroyed) return;
1361
+ this.isDestroyed = true;
1362
+ this.cancelConnect();
1363
+ this.cancelReconnect();
1364
+ this.disconnectInternal();
1365
+ this.subscribers.clear();
1366
+ this.state = initialState();
1367
+ log3.info("manager destroyed");
1368
+ }
1369
+ // ── Private ──────────────────────────────────────────────────────
1370
+ async connectToProvider(type, signal) {
1371
+ const provider = this.createProvider(type);
1372
+ const result = await provider.connect(signal);
1373
+ if (!result.ok) {
1374
+ provider.disconnect();
1375
+ this.setState({ status: "disconnected", error: result.error });
1376
+ return result;
1377
+ }
1378
+ this.provider = provider;
1379
+ const statusUnsub = provider.onStatusChange((status) => {
1380
+ this.handleProviderStatusChange(status);
1381
+ });
1382
+ this.cleanups.push(statusUnsub);
1383
+ const accountUnsub = provider.onAccountsChange((accounts2) => {
1384
+ this.setState({
1385
+ accounts: accounts2,
1386
+ // Clear selected if no longer in list
1387
+ selectedAccount: accounts2.find((a) => a.address === this.state.selectedAccount?.address) ?? null
1388
+ });
1389
+ });
1390
+ this.cleanups.push(accountUnsub);
1391
+ const accounts = result.value;
1392
+ const persisted = await this.loadPersistedAccount();
1393
+ const restoredAccount = persisted ? accounts.find((a) => a.address === persisted) : null;
1394
+ const selectedAccount = restoredAccount ?? (accounts.length > 0 ? accounts[0] : null);
1395
+ this.setState({
1396
+ status: "connected",
1397
+ accounts,
1398
+ activeProvider: type,
1399
+ selectedAccount,
1400
+ error: null
1401
+ });
1402
+ if (selectedAccount) {
1403
+ this.persistAccount(selectedAccount.address);
1404
+ }
1405
+ log3.info("connected", { provider: type, accounts: accounts.length });
1406
+ return result;
1407
+ }
1408
+ createProvider(type) {
1409
+ if (this.providerFactory) {
1410
+ return this.providerFactory(type);
1411
+ }
1412
+ switch (type) {
1413
+ case "host":
1414
+ return new HostProvider({
1415
+ ss58Prefix: this.ss58Prefix,
1416
+ maxRetries: this.maxRetries,
1417
+ retryDelay: 500
1418
+ });
1419
+ case "dev":
1420
+ return new DevProvider({
1421
+ ss58Prefix: this.ss58Prefix
1422
+ });
1423
+ default:
1424
+ throw new Error(
1425
+ `Unsupported provider type: ${type}. The SDK only supports "host" and "dev" providers.`
1426
+ );
1427
+ }
1428
+ }
1429
+ /* @integration */
1430
+ handleProviderStatusChange(status) {
1431
+ if (status === "disconnected" && this.state.status === "connected") {
1432
+ log3.warn("provider disconnected, attempting reconnect");
1433
+ this.attemptReconnect();
1434
+ }
1435
+ }
1436
+ /* @integration */
1437
+ attemptReconnect() {
1438
+ this.cancelReconnect();
1439
+ const providerType = this.state.activeProvider;
1440
+ if (!providerType) return;
1441
+ this.reconnectController = new AbortController();
1442
+ const signal = this.reconnectController.signal;
1443
+ this.setState({ status: "connecting" });
1444
+ withRetry(
1445
+ async () => {
1446
+ if (signal.aborted) {
1447
+ return err(new HostDisconnectedError("Reconnect cancelled"));
1448
+ }
1449
+ this.disconnectInternal();
1450
+ const provider = this.createProvider(providerType);
1451
+ const connectSignal = providerType === "host" ? AbortSignal.any([signal, AbortSignal.timeout(this.hostTimeout)]) : signal;
1452
+ const result = await provider.connect(connectSignal);
1453
+ if (!result.ok) return result;
1454
+ this.provider = provider;
1455
+ const statusUnsub = provider.onStatusChange(
1456
+ (s) => this.handleProviderStatusChange(s)
1457
+ );
1458
+ this.cleanups.push(statusUnsub);
1459
+ const accountUnsub = provider.onAccountsChange((accounts2) => {
1460
+ this.setState({
1461
+ accounts: accounts2,
1462
+ selectedAccount: accounts2.find(
1463
+ (a) => a.address === this.state.selectedAccount?.address
1464
+ ) ?? null
1465
+ });
1466
+ });
1467
+ this.cleanups.push(accountUnsub);
1468
+ const accounts = result.value;
1469
+ this.setState({
1470
+ status: "connected",
1471
+ accounts,
1472
+ activeProvider: providerType,
1473
+ selectedAccount: accounts.find((a) => a.address === this.state.selectedAccount?.address) ?? (accounts.length > 0 ? accounts[0] : null),
1474
+ error: null
1475
+ });
1476
+ log3.info("reconnected", { provider: providerType });
1477
+ return result;
1478
+ },
1479
+ {
1480
+ maxAttempts: RECONNECT_MAX_ATTEMPTS,
1481
+ initialDelay: RECONNECT_INITIAL_DELAY,
1482
+ maxDelay: RECONNECT_MAX_DELAY,
1483
+ signal
1484
+ }
1485
+ ).then((result) => {
1486
+ if (!result.ok && !signal.aborted) {
1487
+ log3.error("reconnect failed after all retries", { error: result.error });
1488
+ this.setState({
1489
+ status: "disconnected",
1490
+ error: new HostDisconnectedError("Reconnect failed after all retries")
1491
+ });
1492
+ }
1493
+ }).catch((cause) => {
1494
+ log3.error("unexpected reconnect error", { cause });
1495
+ this.setState({
1496
+ status: "disconnected",
1497
+ error: new HostDisconnectedError("Reconnect failed unexpectedly")
1498
+ });
1499
+ });
1500
+ }
1501
+ /** Returns the underlying HostProvider if connected via host, or null otherwise. */
1502
+ getHostProvider() {
1503
+ if (this.provider && this.state.activeProvider === "host") {
1504
+ return this.provider;
1505
+ }
1506
+ return null;
1507
+ }
1508
+ cancelConnect() {
1509
+ if (this.connectController) {
1510
+ this.connectController.abort();
1511
+ this.connectController = null;
1512
+ }
1513
+ }
1514
+ cancelReconnect() {
1515
+ if (this.reconnectController) {
1516
+ this.reconnectController.abort();
1517
+ this.reconnectController = null;
1518
+ }
1519
+ }
1520
+ disconnectInternal() {
1521
+ for (const cleanup of this.cleanups) {
1522
+ cleanup();
1523
+ }
1524
+ this.cleanups = [];
1525
+ if (this.provider) {
1526
+ this.provider.disconnect();
1527
+ this.provider = null;
1528
+ }
1529
+ }
1530
+ persistAccount(address) {
1531
+ void this.getPersistence().then((p) => {
1532
+ if (p) {
1533
+ const key = persistenceStorageKey(this.dappName);
1534
+ return Promise.resolve(p.setItem(key, address));
1535
+ }
1536
+ }).catch(() => {
1537
+ log3.debug("failed to persist selected account");
1538
+ });
1539
+ }
1540
+ async loadPersistedAccount() {
1541
+ try {
1542
+ const p = await this.getPersistence();
1543
+ if (!p) return null;
1544
+ const key = persistenceStorageKey(this.dappName);
1545
+ const value = await Promise.resolve(p.getItem(key));
1546
+ return value || null;
1547
+ } catch {
1548
+ log3.debug("failed to load persisted account");
1549
+ return null;
1550
+ }
1551
+ }
1552
+ setState(patch) {
1553
+ this.state = { ...this.state, ...patch };
1554
+ for (const subscriber of this.subscribers) {
1555
+ subscriber(this.state);
1556
+ }
1557
+ }
1558
+ };
1559
+ export {
1560
+ AccountNotFoundError,
1561
+ DestroyedError,
1562
+ DevProvider,
1563
+ HostDisconnectedError,
1564
+ HostProvider,
1565
+ HostRejectedError,
1566
+ HostUnavailableError,
1567
+ NoAccountsError,
1568
+ SignerError,
1569
+ SignerManager,
1570
+ SigningFailedError,
1571
+ TimeoutError,
1572
+ err,
1573
+ isHostError,
1574
+ ok,
1575
+ sleep,
1576
+ withRetry
1577
+ };
1578
+ //# sourceMappingURL=index.js.map