@parity/product-sdk-signer 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as polkadot_api from 'polkadot-api';
2
2
  import { PolkadotSigner } from 'polkadot-api';
3
3
  import { SS58String } from '@parity/product-sdk-address';
4
+ import { AllocatableResource, AllocationOutcome } from '@parity/product-sdk-host';
4
5
 
5
6
  /** Function that unsubscribes a listener when called. */
6
7
  type Unsubscribe = () => void;
@@ -139,7 +140,60 @@ interface SignerManagerOptions {
139
140
  * Set to `null` to disable persistence entirely.
140
141
  */
141
142
  persistence?: AccountPersistence | null;
143
+ /**
144
+ * Callback fired exactly when the manager transitions to `connected`
145
+ * with a selected account — not on subsequent state mutations while
146
+ * still connected. Fires again after auto-reconnect, so a fresh host
147
+ * session re-runs the callback.
148
+ *
149
+ * Common use: request product resource allocations once per session.
150
+ * The `ctx` exposes a pre-bound `requestResourceAllocation` helper
151
+ * plus an `AbortSignal` that fires if the user disconnects or
152
+ * destroys the manager mid-flight.
153
+ *
154
+ * `requestResourceAllocation` throws on failure (matches the
155
+ * `@parity/product-sdk-host` export of the same name); errors thrown
156
+ * from `onConnect` are logged but do not affect the connected state —
157
+ * the next reconnect retries.
158
+ *
159
+ * @example
160
+ * ```ts
161
+ * new SignerManager({
162
+ * onConnect: async (_account, { requestResourceAllocation, signal }) => {
163
+ * try {
164
+ * const outcomes = await requestResourceAllocation([
165
+ * { tag: "AutoSigning", value: undefined },
166
+ * ]);
167
+ * if (signal.aborted) return;
168
+ * if (outcomes.some((o) => o.tag !== "Allocated")) {
169
+ * logWarning("partial permissions", outcomes);
170
+ * }
171
+ * } catch (cause) {
172
+ * logWarning("resource allocation failed", cause);
173
+ * }
174
+ * },
175
+ * });
176
+ * ```
177
+ */
178
+ onConnect?: OnConnect;
142
179
  }
180
+ /** Context passed to the `onConnect` callback. */
181
+ interface ConnectContext {
182
+ /**
183
+ * Aborted when the manager disconnects or is destroyed while the
184
+ * callback is still running. Pass through to `fetch` / cancellation
185
+ * primitives so mid-flight work stops promptly.
186
+ */
187
+ signal: AbortSignal;
188
+ /**
189
+ * Request a batch of host resource allocations. Bound shorthand for
190
+ * `requestResourceAllocation` from `@parity/product-sdk-host` —
191
+ * throws on failure, returns the unwrapped outcomes on success.
192
+ */
193
+ requestResourceAllocation: (resources: AllocatableResource[]) => Promise<AllocationOutcome[]>;
194
+ }
195
+ /** Callback signature for {@link SignerManagerOptions.onConnect}. */
196
+ type OnConnect = (account: SignerAccount, ctx: ConnectContext) => void | Promise<void>;
143
197
 
144
198
  /** Base class for all signer errors. Use `instanceof SignerError` to catch any signer-related error. */
145
199
  declare class SignerError extends Error {
@@ -367,22 +421,69 @@ declare class HostProvider implements SignerProvider {
367
421
  * Core orchestrator for signer management.
368
422
  *
369
423
  * Manages account discovery and signer creation via the Host API.
370
- * Framework-agnostic — use the subscribe() pattern to integrate with
424
+ * Framework-agnostic — use the `subscribe()` pattern to integrate with
371
425
  * React, Vue, or any framework.
372
426
  *
427
+ * ## Lifecycle
428
+ *
429
+ * ```
430
+ * disconnected → connecting → connected ──── selectAccount / signRaw / …
431
+ * ▲ │ │
432
+ * │ ▼ ▼
433
+ * └── disconnect() provider drops → auto-reconnect → connected
434
+ * (onConnect re-fires)
435
+ *
436
+ * ┌─ destroy() ──► (terminal — manager unusable)
437
+ * ▼
438
+ * ```
439
+ *
440
+ * ## Callbacks
441
+ *
442
+ * - **`subscribe(cb)`** fires synchronously on every state mutation, in
443
+ * registration order, inside the call stack that mutated state. It does
444
+ * **not** fire with the initial state — call `getState()` if you need a
445
+ * priming read. Multiple mutations during the same operation produce
446
+ * multiple notifications.
447
+ *
448
+ * - **`onConnect(account, ctx)`** (from `SignerManagerOptions`) fires
449
+ * exactly when the manager transitions from non-connected to
450
+ * `"connected"` with a selected account. It fires on a microtask
451
+ * *after* the `subscribe` notification, so subscribers always observe
452
+ * `state.status === "connected"` before `onConnect`'s side effects run.
453
+ * It re-fires after auto-reconnect (the SDK reconnects automatically
454
+ * when the provider drops), and re-fires after a fresh `connect()`.
455
+ *
456
+ * - **Internal `onAccountsChange` wiring** is worth a behavioral note:
457
+ * when the provider reports an updated account list, the manager
458
+ * preserves the current selection if its address is still present, or
459
+ * sets `selectedAccount` to `null` if it isn't — it does **not** fall
460
+ * back to `accounts[0]`. The fallback-to-first only applies on
461
+ * connect-success, where there is no prior selection to preserve.
462
+ *
463
+ * ## `disconnect()` vs `destroy()`
464
+ *
465
+ * - `disconnect()` resets state to initial. Subsequent `connect()` calls
466
+ * work normally. Reversible.
467
+ * - `destroy()` is **terminal**: the instance is marked unusable, all
468
+ * subscribers are cleared, and any further call returns `DestroyedError`.
469
+ * Use in framework teardown (React `useEffect` cleanup, HMR dispose).
470
+ *
471
+ * Both methods cancel any in-flight `connect`, reconnect attempt, and
472
+ * `onConnect` callback (the `ctx.signal` becomes aborted).
473
+ *
373
474
  * @example
374
475
  * ```ts
375
- * const manager = new SignerManager();
476
+ * const manager = new SignerManager({
477
+ * onConnect: async (_account, { requestResourceAllocation }) => {
478
+ * await requestResourceAllocation([{ tag: "AutoSigning", value: undefined }]);
479
+ * },
480
+ * });
376
481
  * manager.subscribe(state => console.log(state.status));
377
482
  *
378
- * // Connect to the host provider
379
- * await manager.connect();
483
+ * await manager.connect(); // host provider (default)
484
+ * await manager.connect("dev"); // Alice / Bob / … for testing
380
485
  *
381
- * // Or use dev accounts for testing
382
- * await manager.connect("dev");
383
- *
384
- * // Select account and get signer
385
- * manager.selectAccount("5GrwvaEF...");
486
+ * manager.selectAccount("5GrwvaEF…");
386
487
  * const signer = manager.getSigner();
387
488
  * ```
388
489
  */
@@ -401,13 +502,26 @@ declare class SignerManager {
401
502
  private readonly dappName;
402
503
  private readonly persistenceOption;
403
504
  private resolvedPersistence;
505
+ private readonly onConnectCallback;
506
+ private onConnectController;
404
507
  constructor(options?: SignerManagerOptions);
405
508
  private getPersistence;
406
509
  /** Get a snapshot of the current state. */
407
510
  getState(): SignerState;
408
511
  /**
409
- * Subscribe to state changes. The callback fires on every state mutation.
410
- * Returns an unsubscribe function.
512
+ * Subscribe to state changes.
513
+ *
514
+ * The callback fires synchronously on every state mutation, in
515
+ * registration order, inside the call stack that mutated state. It
516
+ * does **not** fire with the current state at subscription time —
517
+ * call {@link getState} if you need a priming read.
518
+ *
519
+ * For "fired once when the user connects" semantics, prefer the
520
+ * {@link SignerManagerOptions.onConnect} option instead of gating on
521
+ * `state.status` inside this callback — `subscribe` will fire many
522
+ * times while connected (`selectAccount`, account swaps, etc.).
523
+ *
524
+ * @returns an unsubscribe function.
411
525
  */
412
526
  subscribe(callback: (state: SignerState) => void): () => void;
413
527
  /**
@@ -421,7 +535,13 @@ declare class SignerManager {
421
535
  * - `"dev"`: Connect using dev accounts (for testing)
422
536
  */
423
537
  connect(providerType?: ProviderType): Promise<Result<SignerAccount[], SignerError>>;
424
- /** Disconnect from the current provider and reset state. */
538
+ /**
539
+ * Disconnect from the current provider and reset state to initial.
540
+ *
541
+ * Reversible — subsequent `connect()` calls work normally. Cancels
542
+ * any in-flight `connect`, reconnect attempt, or `onConnect` callback
543
+ * (`ctx.signal` becomes aborted).
544
+ */
425
545
  disconnect(): void;
426
546
  /**
427
547
  * Select an account by address.
@@ -477,7 +597,12 @@ declare class SignerManager {
477
597
  createRingVRFProof(dotNsIdentifier: string, derivationIndex: number, location: RingLocation, message: Uint8Array): Promise<Result<Uint8Array, SignerError>>;
478
598
  /**
479
599
  * Destroy the manager and release all resources.
480
- * After calling destroy(), the manager is unusable.
600
+ *
601
+ * **Terminal** — clears all subscribers, cancels in-flight work, and
602
+ * marks the instance unusable. Any subsequent method returns
603
+ * `DestroyedError`. Idempotent. Use in framework teardown (React
604
+ * `useEffect` cleanup, HMR dispose). For a reversible reset, use
605
+ * {@link disconnect} instead.
481
606
  */
482
607
  destroy(): void;
483
608
  private connectToProvider;
@@ -488,6 +613,8 @@ declare class SignerManager {
488
613
  private getHostProvider;
489
614
  private cancelConnect;
490
615
  private cancelReconnect;
616
+ private cancelOnConnect;
617
+ private fireOnConnect;
491
618
  private disconnectInternal;
492
619
  private persistAccount;
493
620
  private loadPersistedAccount;
@@ -561,4 +688,4 @@ declare class DevProvider implements SignerProvider {
561
688
  onAccountsChange(_callback: (accounts: SignerAccount[]) => void): Unsubscribe;
562
689
  }
563
690
 
564
- export { AccountNotFoundError, type AccountPersistence, type ConnectionStatus, type ContextualAlias, DestroyedError, type DevAccountName, type DevKeyType, DevProvider, type DevProviderOptions, HostDisconnectedError, HostProvider, type HostProviderOptions, HostRejectedError, HostUnavailableError, NoAccountsError, type ProductAccount, type ProviderFactory, type ProviderType, type Result, type RetryOptions, type RingLocation, type SignerAccount, SignerError, SignerManager, type SignerManagerOptions, type SignerProvider, type SignerState, SigningFailedError, TimeoutError, type Unsubscribe, err, isHostError, ok, sleep, withRetry };
691
+ export { AccountNotFoundError, type AccountPersistence, type ConnectContext, type ConnectionStatus, type ContextualAlias, DestroyedError, type DevAccountName, type DevKeyType, DevProvider, type DevProviderOptions, HostDisconnectedError, HostProvider, type HostProviderOptions, HostRejectedError, HostUnavailableError, NoAccountsError, type OnConnect, type ProductAccount, type ProviderFactory, type ProviderType, type Result, type RetryOptions, type RingLocation, type SignerAccount, SignerError, SignerManager, type SignerManagerOptions, type SignerProvider, type SignerState, SigningFailedError, TimeoutError, type Unsubscribe, err, isHostError, ok, sleep, withRetry };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createLogger } from '@parity/product-sdk-logger';
2
- import { getHostLocalStorage } from '@parity/product-sdk-host';
2
+ import { requestResourceAllocation, getHostLocalStorage } from '@parity/product-sdk-host';
3
3
  import { seedToAccount } from '@parity/product-sdk-keys';
4
4
  import { ss58Encode, deriveH160 } from '@parity/product-sdk-address';
5
5
 
@@ -517,6 +517,13 @@ function initialState() {
517
517
  error: null
518
518
  };
519
519
  }
520
+ function resolveSelectedAccount(accounts, preferredAddress) {
521
+ if (preferredAddress) {
522
+ const found = accounts.find((a) => a.address === preferredAddress);
523
+ if (found) return found;
524
+ }
525
+ return accounts[0] ?? null;
526
+ }
520
527
  var SignerManager = class {
521
528
  state;
522
529
  provider = null;
@@ -532,6 +539,8 @@ var SignerManager = class {
532
539
  dappName;
533
540
  persistenceOption;
534
541
  resolvedPersistence;
542
+ onConnectCallback;
543
+ onConnectController = null;
535
544
  constructor(options) {
536
545
  this.ss58Prefix = options?.ss58Prefix ?? DEFAULT_SS58_PREFIX;
537
546
  this.hostTimeout = options?.hostTimeout ?? DEFAULT_HOST_TIMEOUT;
@@ -540,6 +549,7 @@ var SignerManager = class {
540
549
  this.dappName = options?.dappName ?? DEFAULT_DAPP_NAME;
541
550
  this.persistenceOption = options?.persistence;
542
551
  this.resolvedPersistence = options?.persistence;
552
+ this.onConnectCallback = options?.onConnect;
543
553
  this.state = initialState();
544
554
  }
545
555
  async getPersistence() {
@@ -555,8 +565,19 @@ var SignerManager = class {
555
565
  return this.state;
556
566
  }
557
567
  /**
558
- * Subscribe to state changes. The callback fires on every state mutation.
559
- * Returns an unsubscribe function.
568
+ * Subscribe to state changes.
569
+ *
570
+ * The callback fires synchronously on every state mutation, in
571
+ * registration order, inside the call stack that mutated state. It
572
+ * does **not** fire with the current state at subscription time —
573
+ * call {@link getState} if you need a priming read.
574
+ *
575
+ * For "fired once when the user connects" semantics, prefer the
576
+ * {@link SignerManagerOptions.onConnect} option instead of gating on
577
+ * `state.status` inside this callback — `subscribe` will fire many
578
+ * times while connected (`selectAccount`, account swaps, etc.).
579
+ *
580
+ * @returns an unsubscribe function.
560
581
  */
561
582
  subscribe(callback) {
562
583
  this.subscribers.add(callback);
@@ -580,6 +601,7 @@ var SignerManager = class {
580
601
  }
581
602
  this.cancelConnect();
582
603
  this.cancelReconnect();
604
+ this.cancelOnConnect();
583
605
  this.connectController = new AbortController();
584
606
  const signal = this.connectController.signal;
585
607
  this.disconnectInternal();
@@ -587,10 +609,17 @@ var SignerManager = class {
587
609
  const targetProvider = providerType ?? "host";
588
610
  return this.connectToProvider(targetProvider, signal);
589
611
  }
590
- /** Disconnect from the current provider and reset state. */
612
+ /**
613
+ * Disconnect from the current provider and reset state to initial.
614
+ *
615
+ * Reversible — subsequent `connect()` calls work normally. Cancels
616
+ * any in-flight `connect`, reconnect attempt, or `onConnect` callback
617
+ * (`ctx.signal` becomes aborted).
618
+ */
591
619
  disconnect() {
592
620
  this.cancelConnect();
593
621
  this.cancelReconnect();
622
+ this.cancelOnConnect();
594
623
  this.disconnectInternal();
595
624
  this.setState(initialState());
596
625
  log3.info("disconnected");
@@ -709,13 +738,19 @@ var SignerManager = class {
709
738
  }
710
739
  /**
711
740
  * Destroy the manager and release all resources.
712
- * After calling destroy(), the manager is unusable.
741
+ *
742
+ * **Terminal** — clears all subscribers, cancels in-flight work, and
743
+ * marks the instance unusable. Any subsequent method returns
744
+ * `DestroyedError`. Idempotent. Use in framework teardown (React
745
+ * `useEffect` cleanup, HMR dispose). For a reversible reset, use
746
+ * {@link disconnect} instead.
713
747
  */
714
748
  destroy() {
715
749
  if (this.isDestroyed) return;
716
750
  this.isDestroyed = true;
717
751
  this.cancelConnect();
718
752
  this.cancelReconnect();
753
+ this.cancelOnConnect();
719
754
  this.disconnectInternal();
720
755
  this.subscribers.clear();
721
756
  this.state = initialState();
@@ -745,8 +780,7 @@ var SignerManager = class {
745
780
  this.cleanups.push(accountUnsub);
746
781
  const accounts = result.value;
747
782
  const persisted = await this.loadPersistedAccount();
748
- const restoredAccount = persisted ? accounts.find((a) => a.address === persisted) : null;
749
- const selectedAccount = restoredAccount ?? (accounts.length > 0 ? accounts[0] : null);
783
+ const selectedAccount = resolveSelectedAccount(accounts, persisted);
750
784
  this.setState({
751
785
  status: "connected",
752
786
  accounts,
@@ -756,6 +790,7 @@ var SignerManager = class {
756
790
  });
757
791
  if (selectedAccount) {
758
792
  this.persistAccount(selectedAccount.address);
793
+ this.fireOnConnect(selectedAccount);
759
794
  }
760
795
  log3.info("connected", { provider: type, accounts: accounts.length });
761
796
  return result;
@@ -821,13 +856,20 @@ var SignerManager = class {
821
856
  });
822
857
  this.cleanups.push(accountUnsub);
823
858
  const accounts = result.value;
859
+ const selectedAccount = resolveSelectedAccount(
860
+ accounts,
861
+ this.state.selectedAccount?.address
862
+ );
824
863
  this.setState({
825
864
  status: "connected",
826
865
  accounts,
827
866
  activeProvider: providerType,
828
- selectedAccount: accounts.find((a) => a.address === this.state.selectedAccount?.address) ?? (accounts.length > 0 ? accounts[0] : null),
867
+ selectedAccount,
829
868
  error: null
830
869
  });
870
+ if (selectedAccount) {
871
+ this.fireOnConnect(selectedAccount);
872
+ }
831
873
  log3.info("reconnected", { provider: providerType });
832
874
  return result;
833
875
  },
@@ -861,16 +903,38 @@ var SignerManager = class {
861
903
  return null;
862
904
  }
863
905
  cancelConnect() {
864
- if (this.connectController) {
865
- this.connectController.abort();
866
- this.connectController = null;
867
- }
906
+ this.connectController?.abort();
907
+ this.connectController = null;
868
908
  }
869
909
  cancelReconnect() {
870
- if (this.reconnectController) {
871
- this.reconnectController.abort();
872
- this.reconnectController = null;
873
- }
910
+ this.reconnectController?.abort();
911
+ this.reconnectController = null;
912
+ }
913
+ cancelOnConnect() {
914
+ this.onConnectController?.abort();
915
+ this.onConnectController = null;
916
+ }
917
+ fireOnConnect(account) {
918
+ const callback = this.onConnectCallback;
919
+ if (!callback) return;
920
+ this.cancelOnConnect();
921
+ const controller = new AbortController();
922
+ this.onConnectController = controller;
923
+ const ctx = {
924
+ signal: controller.signal,
925
+ requestResourceAllocation
926
+ };
927
+ queueMicrotask(async () => {
928
+ try {
929
+ await callback(account, ctx);
930
+ } catch (cause) {
931
+ log3.warn("onConnect callback threw", { cause });
932
+ } finally {
933
+ if (this.onConnectController === controller) {
934
+ this.onConnectController = null;
935
+ }
936
+ }
937
+ });
874
938
  }
875
939
  disconnectInternal() {
876
940
  for (const cleanup of this.cleanups) {