@parity/product-sdk-signer 0.2.4 → 0.4.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 {
@@ -193,7 +247,7 @@ interface HostProviderOptions {
193
247
  /** Initial retry delay in ms. Default: 500 */
194
248
  retryDelay?: number;
195
249
  /**
196
- * Custom SDK loader. Defaults to `import("@novasamatech/product-sdk")`.
250
+ * Custom SDK loader. Defaults to `import("@novasamatech/host-api-wrapper")`.
197
251
  * Override this for testing or custom SDK setups.
198
252
  * @internal
199
253
  */
@@ -268,7 +322,7 @@ interface AccountsProvider {
268
322
  getLegacyAccounts: () => NeverthrowResultAsync<RawAccount[], unknown>;
269
323
  getLegacyAccountSigner: (account: ProductAccount) => polkadot_api.PolkadotSigner;
270
324
  getProductAccount: (dotNsIdentifier: string, derivationIndex?: number) => NeverthrowResultAsync<RawAccount, unknown>;
271
- getProductAccountSigner: (account: ProductAccount) => polkadot_api.PolkadotSigner;
325
+ getProductAccountSigner: (account: ProductAccount, signerType?: "signPayload" | "createTransaction") => polkadot_api.PolkadotSigner;
272
326
  getProductAccountAlias: (dotNsIdentifier: string, derivationIndex?: number) => NeverthrowResultAsync<ContextualAlias, unknown>;
273
327
  createRingVRFProof: (dotNsIdentifier: string, derivationIndex: number, location: unknown, message: Uint8Array) => NeverthrowResultAsync<Uint8Array, unknown>;
274
328
  subscribeAccountConnectionStatus: (callback: (status: string) => void) => {
@@ -300,7 +354,7 @@ interface ProductSdkModule {
300
354
  /**
301
355
  * Provider for the Host API (Polkadot Desktop / Android).
302
356
  *
303
- * Dynamically imports `@novasamatech/product-sdk` at runtime so it remains
357
+ * Dynamically imports `@novasamatech/host-api-wrapper` at runtime so it remains
304
358
  * an optional peer dependency. Apps running outside a host container will
305
359
  * gracefully get a `HOST_UNAVAILABLE` error.
306
360
  *
@@ -339,6 +393,12 @@ declare class HostProvider implements SignerProvider {
339
393
  *
340
394
  * Convenience method for when you already have the product account details.
341
395
  * Requires a prior successful `connect()` call.
396
+ *
397
+ * Routing is pinned to `signerType: "createTransaction"` via
398
+ * {@link PRODUCT_SIGNER_TYPE} so unknown signed extensions (e.g. `AsPgas`
399
+ * on Paseo Next) are forwarded to the host as opaque bytes for
400
+ * metadata-driven decoding, rather than going through the PJS bridge
401
+ * that throws on unknown extensions.
342
402
  */
343
403
  getProductAccountSigner(account: ProductAccount): polkadot_api.PolkadotSigner;
344
404
  /**
@@ -367,22 +427,69 @@ declare class HostProvider implements SignerProvider {
367
427
  * Core orchestrator for signer management.
368
428
  *
369
429
  * Manages account discovery and signer creation via the Host API.
370
- * Framework-agnostic — use the subscribe() pattern to integrate with
430
+ * Framework-agnostic — use the `subscribe()` pattern to integrate with
371
431
  * React, Vue, or any framework.
372
432
  *
433
+ * ## Lifecycle
434
+ *
435
+ * ```
436
+ * disconnected → connecting → connected ──── selectAccount / signRaw / …
437
+ * ▲ │ │
438
+ * │ ▼ ▼
439
+ * └── disconnect() provider drops → auto-reconnect → connected
440
+ * (onConnect re-fires)
441
+ *
442
+ * ┌─ destroy() ──► (terminal — manager unusable)
443
+ * ▼
444
+ * ```
445
+ *
446
+ * ## Callbacks
447
+ *
448
+ * - **`subscribe(cb)`** fires synchronously on every state mutation, in
449
+ * registration order, inside the call stack that mutated state. It does
450
+ * **not** fire with the initial state — call `getState()` if you need a
451
+ * priming read. Multiple mutations during the same operation produce
452
+ * multiple notifications.
453
+ *
454
+ * - **`onConnect(account, ctx)`** (from `SignerManagerOptions`) fires
455
+ * exactly when the manager transitions from non-connected to
456
+ * `"connected"` with a selected account. It fires on a microtask
457
+ * *after* the `subscribe` notification, so subscribers always observe
458
+ * `state.status === "connected"` before `onConnect`'s side effects run.
459
+ * It re-fires after auto-reconnect (the SDK reconnects automatically
460
+ * when the provider drops), and re-fires after a fresh `connect()`.
461
+ *
462
+ * - **Internal `onAccountsChange` wiring** is worth a behavioral note:
463
+ * when the provider reports an updated account list, the manager
464
+ * preserves the current selection if its address is still present, or
465
+ * sets `selectedAccount` to `null` if it isn't — it does **not** fall
466
+ * back to `accounts[0]`. The fallback-to-first only applies on
467
+ * connect-success, where there is no prior selection to preserve.
468
+ *
469
+ * ## `disconnect()` vs `destroy()`
470
+ *
471
+ * - `disconnect()` resets state to initial. Subsequent `connect()` calls
472
+ * work normally. Reversible.
473
+ * - `destroy()` is **terminal**: the instance is marked unusable, all
474
+ * subscribers are cleared, and any further call returns `DestroyedError`.
475
+ * Use in framework teardown (React `useEffect` cleanup, HMR dispose).
476
+ *
477
+ * Both methods cancel any in-flight `connect`, reconnect attempt, and
478
+ * `onConnect` callback (the `ctx.signal` becomes aborted).
479
+ *
373
480
  * @example
374
481
  * ```ts
375
- * const manager = new SignerManager();
482
+ * const manager = new SignerManager({
483
+ * onConnect: async (_account, { requestResourceAllocation }) => {
484
+ * await requestResourceAllocation([{ tag: "AutoSigning", value: undefined }]);
485
+ * },
486
+ * });
376
487
  * manager.subscribe(state => console.log(state.status));
377
488
  *
378
- * // Connect to the host provider
379
- * await manager.connect();
489
+ * await manager.connect(); // host provider (default)
490
+ * await manager.connect("dev"); // Alice / Bob / … for testing
380
491
  *
381
- * // Or use dev accounts for testing
382
- * await manager.connect("dev");
383
- *
384
- * // Select account and get signer
385
- * manager.selectAccount("5GrwvaEF...");
492
+ * manager.selectAccount("5GrwvaEF…");
386
493
  * const signer = manager.getSigner();
387
494
  * ```
388
495
  */
@@ -401,13 +508,26 @@ declare class SignerManager {
401
508
  private readonly dappName;
402
509
  private readonly persistenceOption;
403
510
  private resolvedPersistence;
511
+ private readonly onConnectCallback;
512
+ private onConnectController;
404
513
  constructor(options?: SignerManagerOptions);
405
514
  private getPersistence;
406
515
  /** Get a snapshot of the current state. */
407
516
  getState(): SignerState;
408
517
  /**
409
- * Subscribe to state changes. The callback fires on every state mutation.
410
- * Returns an unsubscribe function.
518
+ * Subscribe to state changes.
519
+ *
520
+ * The callback fires synchronously on every state mutation, in
521
+ * registration order, inside the call stack that mutated state. It
522
+ * does **not** fire with the current state at subscription time —
523
+ * call {@link getState} if you need a priming read.
524
+ *
525
+ * For "fired once when the user connects" semantics, prefer the
526
+ * {@link SignerManagerOptions.onConnect} option instead of gating on
527
+ * `state.status` inside this callback — `subscribe` will fire many
528
+ * times while connected (`selectAccount`, account swaps, etc.).
529
+ *
530
+ * @returns an unsubscribe function.
411
531
  */
412
532
  subscribe(callback: (state: SignerState) => void): () => void;
413
533
  /**
@@ -421,7 +541,13 @@ declare class SignerManager {
421
541
  * - `"dev"`: Connect using dev accounts (for testing)
422
542
  */
423
543
  connect(providerType?: ProviderType): Promise<Result<SignerAccount[], SignerError>>;
424
- /** Disconnect from the current provider and reset state. */
544
+ /**
545
+ * Disconnect from the current provider and reset state to initial.
546
+ *
547
+ * Reversible — subsequent `connect()` calls work normally. Cancels
548
+ * any in-flight `connect`, reconnect attempt, or `onConnect` callback
549
+ * (`ctx.signal` becomes aborted).
550
+ */
425
551
  disconnect(): void;
426
552
  /**
427
553
  * Select an account by address.
@@ -477,7 +603,12 @@ declare class SignerManager {
477
603
  createRingVRFProof(dotNsIdentifier: string, derivationIndex: number, location: RingLocation, message: Uint8Array): Promise<Result<Uint8Array, SignerError>>;
478
604
  /**
479
605
  * Destroy the manager and release all resources.
480
- * After calling destroy(), the manager is unusable.
606
+ *
607
+ * **Terminal** — clears all subscribers, cancels in-flight work, and
608
+ * marks the instance unusable. Any subsequent method returns
609
+ * `DestroyedError`. Idempotent. Use in framework teardown (React
610
+ * `useEffect` cleanup, HMR dispose). For a reversible reset, use
611
+ * {@link disconnect} instead.
481
612
  */
482
613
  destroy(): void;
483
614
  private connectToProvider;
@@ -488,6 +619,8 @@ declare class SignerManager {
488
619
  private getHostProvider;
489
620
  private cancelConnect;
490
621
  private cancelReconnect;
622
+ private cancelOnConnect;
623
+ private fireOnConnect;
491
624
  private disconnectInternal;
492
625
  private persistAccount;
493
626
  private loadPersistedAccount;
@@ -561,4 +694,4 @@ declare class DevProvider implements SignerProvider {
561
694
  onAccountsChange(_callback: (accounts: SignerAccount[]) => void): Unsubscribe;
562
695
  }
563
696
 
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 };
697
+ 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
 
@@ -180,8 +180,9 @@ async function withRetry(fn, options) {
180
180
 
181
181
  // src/providers/host.ts
182
182
  var log2 = createLogger("signer:host");
183
+ var PRODUCT_SIGNER_TYPE = "createTransaction";
183
184
  async function defaultLoadSdk() {
184
- return await import('@novasamatech/product-sdk');
185
+ return await import('@novasamatech/host-api-wrapper');
185
186
  }
186
187
  async function defaultLoadHostApiEnum() {
187
188
  return await import('@novasamatech/host-api');
@@ -283,7 +284,10 @@ var HostProvider = class {
283
284
  if (!this.accountsProvider) {
284
285
  throw new Error("Host provider is disconnected");
285
286
  }
286
- return this.accountsProvider.getProductAccountSigner(productAccount);
287
+ return this.accountsProvider.getProductAccountSigner(
288
+ productAccount,
289
+ PRODUCT_SIGNER_TYPE
290
+ );
287
291
  }
288
292
  });
289
293
  } catch (cause) {
@@ -300,12 +304,18 @@ var HostProvider = class {
300
304
  *
301
305
  * Convenience method for when you already have the product account details.
302
306
  * Requires a prior successful `connect()` call.
307
+ *
308
+ * Routing is pinned to `signerType: "createTransaction"` via
309
+ * {@link PRODUCT_SIGNER_TYPE} so unknown signed extensions (e.g. `AsPgas`
310
+ * on Paseo Next) are forwarded to the host as opaque bytes for
311
+ * metadata-driven decoding, rather than going through the PJS bridge
312
+ * that throws on unknown extensions.
303
313
  */
304
314
  getProductAccountSigner(account) {
305
315
  if (!this.accountsProvider) {
306
316
  throw new Error("Host provider is not connected");
307
317
  }
308
- return this.accountsProvider.getProductAccountSigner(account);
318
+ return this.accountsProvider.getProductAccountSigner(account, PRODUCT_SIGNER_TYPE);
309
319
  }
310
320
  /**
311
321
  * Get a contextual alias for a product account via Ring VRF.
@@ -517,6 +527,13 @@ function initialState() {
517
527
  error: null
518
528
  };
519
529
  }
530
+ function resolveSelectedAccount(accounts, preferredAddress) {
531
+ if (preferredAddress) {
532
+ const found = accounts.find((a) => a.address === preferredAddress);
533
+ if (found) return found;
534
+ }
535
+ return accounts[0] ?? null;
536
+ }
520
537
  var SignerManager = class {
521
538
  state;
522
539
  provider = null;
@@ -532,6 +549,8 @@ var SignerManager = class {
532
549
  dappName;
533
550
  persistenceOption;
534
551
  resolvedPersistence;
552
+ onConnectCallback;
553
+ onConnectController = null;
535
554
  constructor(options) {
536
555
  this.ss58Prefix = options?.ss58Prefix ?? DEFAULT_SS58_PREFIX;
537
556
  this.hostTimeout = options?.hostTimeout ?? DEFAULT_HOST_TIMEOUT;
@@ -540,6 +559,7 @@ var SignerManager = class {
540
559
  this.dappName = options?.dappName ?? DEFAULT_DAPP_NAME;
541
560
  this.persistenceOption = options?.persistence;
542
561
  this.resolvedPersistence = options?.persistence;
562
+ this.onConnectCallback = options?.onConnect;
543
563
  this.state = initialState();
544
564
  }
545
565
  async getPersistence() {
@@ -555,8 +575,19 @@ var SignerManager = class {
555
575
  return this.state;
556
576
  }
557
577
  /**
558
- * Subscribe to state changes. The callback fires on every state mutation.
559
- * Returns an unsubscribe function.
578
+ * Subscribe to state changes.
579
+ *
580
+ * The callback fires synchronously on every state mutation, in
581
+ * registration order, inside the call stack that mutated state. It
582
+ * does **not** fire with the current state at subscription time —
583
+ * call {@link getState} if you need a priming read.
584
+ *
585
+ * For "fired once when the user connects" semantics, prefer the
586
+ * {@link SignerManagerOptions.onConnect} option instead of gating on
587
+ * `state.status` inside this callback — `subscribe` will fire many
588
+ * times while connected (`selectAccount`, account swaps, etc.).
589
+ *
590
+ * @returns an unsubscribe function.
560
591
  */
561
592
  subscribe(callback) {
562
593
  this.subscribers.add(callback);
@@ -580,6 +611,7 @@ var SignerManager = class {
580
611
  }
581
612
  this.cancelConnect();
582
613
  this.cancelReconnect();
614
+ this.cancelOnConnect();
583
615
  this.connectController = new AbortController();
584
616
  const signal = this.connectController.signal;
585
617
  this.disconnectInternal();
@@ -587,10 +619,17 @@ var SignerManager = class {
587
619
  const targetProvider = providerType ?? "host";
588
620
  return this.connectToProvider(targetProvider, signal);
589
621
  }
590
- /** Disconnect from the current provider and reset state. */
622
+ /**
623
+ * Disconnect from the current provider and reset state to initial.
624
+ *
625
+ * Reversible — subsequent `connect()` calls work normally. Cancels
626
+ * any in-flight `connect`, reconnect attempt, or `onConnect` callback
627
+ * (`ctx.signal` becomes aborted).
628
+ */
591
629
  disconnect() {
592
630
  this.cancelConnect();
593
631
  this.cancelReconnect();
632
+ this.cancelOnConnect();
594
633
  this.disconnectInternal();
595
634
  this.setState(initialState());
596
635
  log3.info("disconnected");
@@ -709,13 +748,19 @@ var SignerManager = class {
709
748
  }
710
749
  /**
711
750
  * Destroy the manager and release all resources.
712
- * After calling destroy(), the manager is unusable.
751
+ *
752
+ * **Terminal** — clears all subscribers, cancels in-flight work, and
753
+ * marks the instance unusable. Any subsequent method returns
754
+ * `DestroyedError`. Idempotent. Use in framework teardown (React
755
+ * `useEffect` cleanup, HMR dispose). For a reversible reset, use
756
+ * {@link disconnect} instead.
713
757
  */
714
758
  destroy() {
715
759
  if (this.isDestroyed) return;
716
760
  this.isDestroyed = true;
717
761
  this.cancelConnect();
718
762
  this.cancelReconnect();
763
+ this.cancelOnConnect();
719
764
  this.disconnectInternal();
720
765
  this.subscribers.clear();
721
766
  this.state = initialState();
@@ -745,8 +790,7 @@ var SignerManager = class {
745
790
  this.cleanups.push(accountUnsub);
746
791
  const accounts = result.value;
747
792
  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);
793
+ const selectedAccount = resolveSelectedAccount(accounts, persisted);
750
794
  this.setState({
751
795
  status: "connected",
752
796
  accounts,
@@ -756,6 +800,7 @@ var SignerManager = class {
756
800
  });
757
801
  if (selectedAccount) {
758
802
  this.persistAccount(selectedAccount.address);
803
+ this.fireOnConnect(selectedAccount);
759
804
  }
760
805
  log3.info("connected", { provider: type, accounts: accounts.length });
761
806
  return result;
@@ -821,13 +866,20 @@ var SignerManager = class {
821
866
  });
822
867
  this.cleanups.push(accountUnsub);
823
868
  const accounts = result.value;
869
+ const selectedAccount = resolveSelectedAccount(
870
+ accounts,
871
+ this.state.selectedAccount?.address
872
+ );
824
873
  this.setState({
825
874
  status: "connected",
826
875
  accounts,
827
876
  activeProvider: providerType,
828
- selectedAccount: accounts.find((a) => a.address === this.state.selectedAccount?.address) ?? (accounts.length > 0 ? accounts[0] : null),
877
+ selectedAccount,
829
878
  error: null
830
879
  });
880
+ if (selectedAccount) {
881
+ this.fireOnConnect(selectedAccount);
882
+ }
831
883
  log3.info("reconnected", { provider: providerType });
832
884
  return result;
833
885
  },
@@ -861,16 +913,38 @@ var SignerManager = class {
861
913
  return null;
862
914
  }
863
915
  cancelConnect() {
864
- if (this.connectController) {
865
- this.connectController.abort();
866
- this.connectController = null;
867
- }
916
+ this.connectController?.abort();
917
+ this.connectController = null;
868
918
  }
869
919
  cancelReconnect() {
870
- if (this.reconnectController) {
871
- this.reconnectController.abort();
872
- this.reconnectController = null;
873
- }
920
+ this.reconnectController?.abort();
921
+ this.reconnectController = null;
922
+ }
923
+ cancelOnConnect() {
924
+ this.onConnectController?.abort();
925
+ this.onConnectController = null;
926
+ }
927
+ fireOnConnect(account) {
928
+ const callback = this.onConnectCallback;
929
+ if (!callback) return;
930
+ this.cancelOnConnect();
931
+ const controller = new AbortController();
932
+ this.onConnectController = controller;
933
+ const ctx = {
934
+ signal: controller.signal,
935
+ requestResourceAllocation
936
+ };
937
+ queueMicrotask(async () => {
938
+ try {
939
+ await callback(account, ctx);
940
+ } catch (cause) {
941
+ log3.warn("onConnect callback threw", { cause });
942
+ } finally {
943
+ if (this.onConnectController === controller) {
944
+ this.onConnectController = null;
945
+ }
946
+ }
947
+ });
874
948
  }
875
949
  disconnectInternal() {
876
950
  for (const cleanup of this.cleanups) {