@meshconnect/uwc-core 1.0.3 → 1.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.
Files changed (121) hide show
  1. package/README.md +247 -76
  2. package/dist/events.d.ts +241 -5
  3. package/dist/events.d.ts.map +1 -1
  4. package/dist/events.js +77 -1
  5. package/dist/events.js.map +1 -1
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/logger/create-logger.d.ts +32 -0
  11. package/dist/logger/create-logger.d.ts.map +1 -0
  12. package/dist/logger/create-logger.js +79 -0
  13. package/dist/logger/create-logger.js.map +1 -0
  14. package/dist/managers/event-manager.d.ts.map +1 -1
  15. package/dist/managers/event-manager.js +5 -1
  16. package/dist/managers/event-manager.js.map +1 -1
  17. package/dist/observability/connect-observer.d.ts +33 -0
  18. package/dist/observability/connect-observer.d.ts.map +1 -0
  19. package/dist/observability/connect-observer.js +115 -0
  20. package/dist/observability/connect-observer.js.map +1 -0
  21. package/dist/observability/focus-tracker.d.ts +42 -0
  22. package/dist/observability/focus-tracker.d.ts.map +1 -0
  23. package/dist/observability/focus-tracker.js +57 -0
  24. package/dist/observability/focus-tracker.js.map +1 -0
  25. package/dist/observability/instrument-handoff.d.ts +54 -0
  26. package/dist/observability/instrument-handoff.d.ts.map +1 -0
  27. package/dist/observability/instrument-handoff.js +173 -0
  28. package/dist/observability/instrument-handoff.js.map +1 -0
  29. package/dist/observability/lifecycle-bridge.d.ts +69 -0
  30. package/dist/observability/lifecycle-bridge.d.ts.map +1 -0
  31. package/dist/observability/lifecycle-bridge.js +155 -0
  32. package/dist/observability/lifecycle-bridge.js.map +1 -0
  33. package/dist/observability/scrub.d.ts +31 -0
  34. package/dist/observability/scrub.d.ts.map +1 -0
  35. package/dist/observability/scrub.js +167 -0
  36. package/dist/observability/scrub.js.map +1 -0
  37. package/dist/observability/telemetry.d.ts +157 -0
  38. package/dist/observability/telemetry.d.ts.map +1 -0
  39. package/dist/observability/telemetry.js +373 -0
  40. package/dist/observability/telemetry.js.map +1 -0
  41. package/dist/services/connection-service.d.ts +23 -1
  42. package/dist/services/connection-service.d.ts.map +1 -1
  43. package/dist/services/connection-service.js +128 -19
  44. package/dist/services/connection-service.js.map +1 -1
  45. package/dist/services/network-switch-service.d.ts +2 -1
  46. package/dist/services/network-switch-service.d.ts.map +1 -1
  47. package/dist/services/network-switch-service.js +45 -16
  48. package/dist/services/network-switch-service.js.map +1 -1
  49. package/dist/services/signature-service.d.ts +14 -7
  50. package/dist/services/signature-service.d.ts.map +1 -1
  51. package/dist/services/signature-service.js +56 -58
  52. package/dist/services/signature-service.js.map +1 -1
  53. package/dist/services/transaction-service.d.ts +19 -5
  54. package/dist/services/transaction-service.d.ts.map +1 -1
  55. package/dist/services/transaction-service.js +55 -38
  56. package/dist/services/transaction-service.js.map +1 -1
  57. package/dist/universal-wallet-connector.d.ts +56 -1
  58. package/dist/universal-wallet-connector.d.ts.map +1 -1
  59. package/dist/universal-wallet-connector.js +207 -110
  60. package/dist/universal-wallet-connector.js.map +1 -1
  61. package/dist/utils/id.d.ts +2 -0
  62. package/dist/utils/id.d.ts.map +1 -0
  63. package/dist/utils/id.js +41 -0
  64. package/dist/utils/id.js.map +1 -0
  65. package/dist/utils/resolve-session-provider.d.ts +10 -0
  66. package/dist/utils/resolve-session-provider.d.ts.map +1 -0
  67. package/dist/utils/resolve-session-provider.js +21 -0
  68. package/dist/utils/resolve-session-provider.js.map +1 -0
  69. package/dist/utils/to-wallet-error.d.ts +35 -0
  70. package/dist/utils/to-wallet-error.d.ts.map +1 -0
  71. package/dist/utils/to-wallet-error.js +130 -0
  72. package/dist/utils/to-wallet-error.js.map +1 -0
  73. package/dist/utils/validate-wallet-session.d.ts +15 -0
  74. package/dist/utils/validate-wallet-session.d.ts.map +1 -0
  75. package/dist/utils/validate-wallet-session.js +29 -0
  76. package/dist/utils/validate-wallet-session.js.map +1 -0
  77. package/dist/version.d.ts +2 -0
  78. package/dist/version.d.ts.map +1 -0
  79. package/dist/version.js +3 -0
  80. package/dist/version.js.map +1 -0
  81. package/package.json +6 -6
  82. package/src/events.ts +326 -14
  83. package/src/index.ts +10 -0
  84. package/src/logger/create-logger.test.ts +126 -0
  85. package/src/logger/create-logger.ts +112 -0
  86. package/src/managers/event-manager.test.ts +55 -0
  87. package/src/managers/event-manager.ts +5 -1
  88. package/src/observability/connect-observer.test.ts +214 -0
  89. package/src/observability/connect-observer.ts +128 -0
  90. package/src/observability/focus-tracker.test.ts +191 -0
  91. package/src/observability/focus-tracker.ts +96 -0
  92. package/src/observability/instrument-handoff.test.ts +397 -0
  93. package/src/observability/instrument-handoff.ts +231 -0
  94. package/src/observability/lifecycle-bridge.test.ts +398 -0
  95. package/src/observability/lifecycle-bridge.ts +216 -0
  96. package/src/observability/scrub.test.ts +183 -0
  97. package/src/observability/scrub.ts +174 -0
  98. package/src/observability/telemetry.test.ts +549 -0
  99. package/src/observability/telemetry.ts +546 -0
  100. package/src/services/connection-service.test.ts +261 -1
  101. package/src/services/connection-service.ts +167 -38
  102. package/src/services/network-switch-service.test.ts +51 -1
  103. package/src/services/network-switch-service.ts +56 -24
  104. package/src/services/signature-service.test.ts +181 -76
  105. package/src/services/signature-service.ts +75 -85
  106. package/src/services/transaction-service.test.ts +270 -69
  107. package/src/services/transaction-service.ts +74 -51
  108. package/src/universal-wallet-connector.instrumentation-guard.test.ts +199 -0
  109. package/src/universal-wallet-connector.observability.test.ts +610 -0
  110. package/src/universal-wallet-connector.test.ts +27 -146
  111. package/src/universal-wallet-connector.ts +279 -142
  112. package/src/utils/id.test.ts +15 -0
  113. package/src/utils/id.ts +48 -0
  114. package/src/utils/resolve-session-provider.test.ts +54 -0
  115. package/src/utils/resolve-session-provider.ts +25 -0
  116. package/src/utils/to-wallet-error.test.ts +181 -0
  117. package/src/utils/to-wallet-error.ts +152 -0
  118. package/src/utils/validate-wallet-session.test.ts +77 -0
  119. package/src/utils/validate-wallet-session.ts +40 -0
  120. package/src/version.test.ts +11 -0
  121. package/src/version.ts +2 -0
@@ -18,9 +18,9 @@ import type {
18
18
  EVMCapabilities,
19
19
  SignatureType,
20
20
  EIP712TypedData,
21
- WalletError
21
+ LogLevel,
22
+ Logger
22
23
  } from '@meshconnect/uwc-types'
23
- import { WalletConnectorError } from '@meshconnect/uwc-types'
24
24
  import { InjectedConnector } from '@meshconnect/uwc-injected-connector'
25
25
  import { WalletConnectConnector } from '@meshconnect/uwc-wallet-connect-connector'
26
26
  import { TonConnectConnector } from '@meshconnect/uwc-ton-connector'
@@ -33,8 +33,22 @@ import { SignatureService } from './services/signature-service'
33
33
  import { TransactionService } from './services/transaction-service'
34
34
  import { WalletCapabilitiesService } from './services/wallet-capabilities-service'
35
35
  import { createNetworkRpcMap } from './utils/network-rpc-utils'
36
+ import { generateId } from './utils/id'
37
+ import {
38
+ toWalletError,
39
+ isAbortError,
40
+ classifyError
41
+ } from './utils/to-wallet-error'
42
+ import { NoCommonNetworkError } from '@meshconnect/uwc-types'
43
+ import { createLogger, type ManagedLogger } from './logger/create-logger'
44
+ import { connectObserver } from './observability/connect-observer'
45
+ import type { UWCObserver } from './observability/telemetry'
46
+ import { hasHandoffTerminal } from './observability/instrument-handoff'
36
47
  import type { UWCEventName, UWCEventListener, UWCOperation } from './events'
37
48
 
49
+ /** Default observational wallet-response deadline (ms). Matches TON's 60s. */
50
+ const DEFAULT_WALLET_RESPONSE_TIMEOUT_MS = 60_000
51
+
38
52
  /**
39
53
  * Configuration object form of the UniversalWalletConnector constructor.
40
54
  * Prefer this over the positional constructor in new code.
@@ -49,6 +63,38 @@ export interface UWCConfig {
49
63
  walletConnectConfig?: WalletConnectConfig
50
64
  tonConnectConfig?: TonConnectConfig
51
65
  tronConnectorConfig?: TronConnectorConfig
66
+ // ---- Observability (all optional; defaults preserve current behaviour) -----
67
+ /**
68
+ * Vendor-agnostic telemetry sink. When omitted, UWC emits NOTHING externally —
69
+ * zero egress is the security-first default for a sign-only wallet layer. The
70
+ * consumer implements the sink and wires it to their backend (Datadog/…).
71
+ */
72
+ observer?: UWCObserver
73
+ /**
74
+ * Redirect UWC's internal logging into your own logger. Receives raw
75
+ * `(message, ...args)` at/above `logLevel`. When omitted, a console-backed
76
+ * logger is used. LOCAL SINK by contract: args are NOT scrubbed here — do not
77
+ * wire this to a remote transport; use `observer.onLog` (scrubbed) for egress.
78
+ */
79
+ logger?: Logger
80
+ /** Console threshold for the default logger. Default `'warn'`. */
81
+ logLevel?: LogLevel
82
+ /**
83
+ * Consumer correlation id, surfaced as its OWN `correlationId` field on every
84
+ * telemetry record — never aliased onto the generated `sdkSessionId`, so
85
+ * "no id supplied" and "id == session" stay distinguishable. Supply your own
86
+ * (e.g. the consumer's `link_sess_id`) so UWC events join your existing
87
+ * telemetry in Datadog/RUM without a separate mapping. Pass a getter if the
88
+ * id isn't known at construction or can change; resolved per-event, and a
89
+ * throwing/empty getter OMITS the field for that record.
90
+ */
91
+ correlationId?: string | (() => string)
92
+ /**
93
+ * Observational wallet-response deadline (ms) for the `walletTimedOut` marker.
94
+ * `0` disables it. TON is always exempt (it bounds its own response). Default
95
+ * 60_000.
96
+ */
97
+ walletResponseTimeoutMs?: number
52
98
  }
53
99
 
54
100
  /** Common options for user-initiated async operations. */
@@ -75,6 +121,12 @@ export class UniversalWalletConnector {
75
121
  private usingIntegratedBrowser: boolean
76
122
  private detectionComplete = false
77
123
  private networkRpcMap: NetworkRpcMap
124
+ private logger: ManagedLogger
125
+ /** Per-instance correlation id stamped on every telemetry record. */
126
+ private readonly sdkSessionId: string = generateId()
127
+ private walletResponseTimeoutMs: number
128
+ /** Tears down the observer subscriptions; set only when an observer is wired. */
129
+ private observerTeardown: (() => void) | undefined
78
130
 
79
131
  /**
80
132
  * Recommended entry point: returns the shared instance, creating it on the
@@ -152,14 +204,54 @@ export class UniversalWalletConnector {
152
204
  tronConnectorConfig: configuredTronConnectorConfig
153
205
  } = config
154
206
 
207
+ // Observability seam — wire the event bus, logger, and (optional) observer
208
+ // FIRST so even construction-time logs/events are captured. `sdkSessionId` is
209
+ // already assigned as a field initializer.
210
+ this.eventManager = new EventManager()
211
+ this.walletResponseTimeoutMs =
212
+ config.walletResponseTimeoutMs ?? DEFAULT_WALLET_RESPONSE_TIMEOUT_MS
213
+ this.logger = createLogger({
214
+ level: config.logLevel ?? 'warn',
215
+ sink: config.logger,
216
+ // Pre-gate tap → typed `log` event → observer.onLog. Fires for all levels
217
+ // regardless of the console threshold.
218
+ onLog: (level, message, args) =>
219
+ this.eventManager.emit('log', { level, message, args })
220
+ })
221
+ if (config.observer) {
222
+ // Resolve the OPTIONAL consumer correlation id per-event so a consumer can
223
+ // align it to their own session key (e.g. link_sess_id). It is kept
224
+ // separate from the always-present generated `sdkSessionId` — never aliased
225
+ // — so a record can distinguish "no consumer id" from "id == session". A
226
+ // missing/empty value or a throwing getter yields `undefined` (no fallback).
227
+ const cid = config.correlationId
228
+ const getCorrelationId: () => string | undefined =
229
+ typeof cid === 'function'
230
+ ? () => {
231
+ try {
232
+ return cid() || undefined
233
+ } catch {
234
+ return undefined
235
+ }
236
+ }
237
+ : () => cid
238
+ this.observerTeardown = connectObserver(
239
+ this.eventManager,
240
+ config.observer,
241
+ this.sdkSessionId,
242
+ getCorrelationId,
243
+ Date.now,
244
+ this.logger
245
+ )
246
+ }
247
+
155
248
  // Track instance creation and warn on duplicates
156
249
  UniversalWalletConnector.instanceCount++
157
250
  if (
158
251
  UniversalWalletConnector.instanceCount > 1 &&
159
252
  !UniversalWalletConnector.hasWarnedAboutMultipleInstances
160
253
  ) {
161
- // eslint-disable-next-line no-console
162
- console.error(
254
+ this.logger.error(
163
255
  `⚠️ WARNING: Multiple instances of UniversalWalletConnector detected (${UniversalWalletConnector.instanceCount} instances). ` +
164
256
  'This can lead to state inconsistencies and unexpected behavior. ' +
165
257
  'Please ensure you create only one instance and reuse it throughout your application. ' +
@@ -197,7 +289,14 @@ export class UniversalWalletConnector {
197
289
  this.wallets,
198
290
  configuredTonConnectConfig,
199
291
  configuredTronConnectorConfig,
200
- networks
292
+ networks,
293
+ // Surface BridgeChild discovery failures (the Bitstarz "wallet never
294
+ // appeared via the bridge" class) as a telemetry event.
295
+ info =>
296
+ this.eventManager.emit('bridgeError', {
297
+ namespace: info.namespace as Namespace,
298
+ error: { type: 'unknown', message: info.message }
299
+ })
201
300
  )
202
301
  this.connectors.set('injected', this.injectedConnector)
203
302
 
@@ -214,35 +313,51 @@ export class UniversalWalletConnector {
214
313
  'walletConnect',
215
314
  new WalletConnectConnector(
216
315
  configuredWalletConnectConfig,
217
- this.networkRpcMap
316
+ this.networkRpcMap,
317
+ // Surface relay-socket failures (e.g. code 3000 JWT clock skew) that are
318
+ // detached from the connect handoff, as a telemetry event.
319
+ (error: unknown) =>
320
+ this.eventManager.emit('relayError', {
321
+ error: {
322
+ type: 'unknown',
323
+ message: error instanceof Error ? error.message : String(error)
324
+ }
325
+ })
218
326
  )
219
327
  )
220
328
  }
221
329
 
222
330
  this.sessionManager = new SessionManager()
223
- this.eventManager = new EventManager()
224
331
  this.connectionService = new ConnectionService(
225
332
  networks,
226
333
  this.wallets,
227
334
  this.sessionManager,
228
335
  this.connectors,
229
336
  configuredUsingIntegratedBrowser,
230
- this.eventManager
337
+ this.eventManager,
338
+ this.walletResponseTimeoutMs
231
339
  )
232
340
  this.networkSwitchService = new NetworkSwitchService(
233
341
  networks,
234
342
  this.sessionManager,
235
343
  this.connectors,
236
344
  configuredUsingIntegratedBrowser,
237
- this.eventManager
345
+ this.eventManager,
346
+ this.walletResponseTimeoutMs
238
347
  )
239
348
  this.signatureService = new SignatureService(
240
349
  this.sessionManager,
241
- this.connectors
350
+ this.connectors,
351
+ configuredUsingIntegratedBrowser,
352
+ this.eventManager,
353
+ this.walletResponseTimeoutMs
242
354
  )
243
355
  this.transactionService = new TransactionService(
244
356
  this.sessionManager,
245
- this.connectors
357
+ this.connectors,
358
+ configuredUsingIntegratedBrowser,
359
+ this.eventManager,
360
+ this.walletResponseTimeoutMs
246
361
  )
247
362
  this.walletCapabilitiesService = new WalletCapabilitiesService(
248
363
  networks,
@@ -288,7 +403,7 @@ export class UniversalWalletConnector {
288
403
  // call, not the sum.
289
404
  const detectionTasks: Promise<void>[] = []
290
405
  for (const namespace of namespaces) {
291
- for (const [, connector] of this.connectors) {
406
+ for (const [connectionMode, connector] of this.connectors) {
292
407
  if (typeof connector.getAvailableWallets !== 'function') continue
293
408
  detectionTasks.push(
294
409
  Promise.resolve()
@@ -303,8 +418,17 @@ export class UniversalWalletConnector {
303
418
  this.applyDetectedWallet(namespace as Namespace, d)
304
419
  }
305
420
  })
306
- .catch(() => {
307
- // Silently handle errors for individual connector/namespace combinations
421
+ .catch(error => {
422
+ // Was a silent catch now a telemetry signal so a discovery
423
+ // failure (the "why didn't wallet X appear?" class) is observable
424
+ // rather than vanishing. STILL swallowed for control flow: one
425
+ // namespace × connector failing must not abort the others or
426
+ // reject detection.
427
+ this.eventManager.emit('walletDiscoveryFailed', {
428
+ namespace: namespace as Namespace,
429
+ connectionMode,
430
+ error: toWalletError(error)
431
+ })
308
432
  })
309
433
  )
310
434
  }
@@ -317,7 +441,13 @@ export class UniversalWalletConnector {
317
441
  // Mark detection as complete
318
442
  this.detectionComplete = true
319
443
 
320
- this.eventManager.emit('walletsDetected', { wallets: this.wallets })
444
+ // Per-namespace requested-vs-matched summary (counts only, non-PII) so a
445
+ // dashboard can tell "asked for N solana wallets, matched 0" (a metadata /
446
+ // config problem) from "asked for none".
447
+ this.eventManager.emit('walletsDetected', {
448
+ wallets: this.wallets,
449
+ discovery: this.summarizeDiscovery(namespaces)
450
+ })
321
451
  this.eventManager.emit('ready', undefined)
322
452
  } catch (error) {
323
453
  this.detectionComplete = true
@@ -459,6 +589,37 @@ export class UniversalWalletConnector {
459
589
  }
460
590
  }
461
591
 
592
+ /**
593
+ * Per-namespace requested-vs-matched counts after detection. `requested` =
594
+ * configured wallets that declared the namespace; `matched` = those now marked
595
+ * installed. Counts only — never a wallet id or address. Feeds `walletsDetected`
596
+ * so a dashboard can answer "asked for N, matched 0?".
597
+ */
598
+ private summarizeDiscovery(
599
+ namespaces: Set<string>
600
+ ): Record<string, { requested: number; matched: number }> {
601
+ const pickProvider = (w: WalletMetadata) =>
602
+ this.usingIntegratedBrowser
603
+ ? w.integratedBrowserInjectedProvider
604
+ : w.extensionInjectedProvider
605
+
606
+ const summary: Record<string, { requested: number; matched: number }> = {}
607
+ for (const namespace of namespaces) {
608
+ let requested = 0
609
+ let matched = 0
610
+ for (const wallet of this.wallets) {
611
+ const nsMeta = pickProvider(wallet)?.namespaceMetaData?.[
612
+ namespace as Namespace
613
+ ] as { installed?: boolean } | undefined
614
+ if (!nsMeta) continue
615
+ requested++
616
+ if (nsMeta.installed) matched++
617
+ }
618
+ summary[namespace] = { requested, matched }
619
+ }
620
+ return summary
621
+ }
622
+
462
623
  getSession() {
463
624
  return this.sessionManager.getSession()
464
625
  }
@@ -518,6 +679,9 @@ export class UniversalWalletConnector {
518
679
  options?: OperationOptions
519
680
  ): Promise<void> {
520
681
  try {
682
+ // Hand-off instrumentation lives in ConnectionService.connect (emitted
683
+ // after validation, at the real wallet boundary) — not here, so pre-wallet
684
+ // validation failures don't pollute the funnel.
521
685
  await this.connectionService.connect(
522
686
  connectionMode,
523
687
  walletId,
@@ -525,7 +689,7 @@ export class UniversalWalletConnector {
525
689
  options
526
690
  )
527
691
  } catch (error) {
528
- this.emitError(error, 'connect')
692
+ this.emitError(error, 'connect', options?.signal)
529
693
  throw error
530
694
  }
531
695
  }
@@ -538,7 +702,7 @@ export class UniversalWalletConnector {
538
702
  try {
539
703
  await this.connectionService.disconnect(options)
540
704
  } catch (error) {
541
- this.emitError(error, 'disconnect')
705
+ this.emitError(error, 'disconnect', options?.signal)
542
706
  throw error
543
707
  }
544
708
  }
@@ -547,10 +711,23 @@ export class UniversalWalletConnector {
547
711
  networkId: NetworkId,
548
712
  options?: OperationOptions
549
713
  ): Promise<void> {
714
+ // Captured before the switch so a cross-namespace move is detectable after.
715
+ const previousNamespace =
716
+ this.sessionManager.getSession().activeNetwork?.namespace
550
717
  try {
718
+ // Hand-off instrumentation lives in NetworkSwitchService.switchNetwork
719
+ // (emitted after validation, at the real wallet boundary).
551
720
  await this.networkSwitchService.switchNetwork(networkId, options)
721
+ // A cross-namespace switch changes the live provider, so the connect-time
722
+ // lifecycle subscription would keep observing the OLD namespace's provider
723
+ // and label its events with the new session's routing. Re-evaluate it.
724
+ const nextNamespace =
725
+ this.sessionManager.getSession().activeNetwork?.namespace
726
+ if (nextNamespace !== undefined && nextNamespace !== previousNamespace) {
727
+ this.connectionService.reattachLifecycle()
728
+ }
552
729
  } catch (error) {
553
- this.emitError(error, 'switchNetwork')
730
+ this.emitError(error, 'switchNetwork', options?.signal)
554
731
  throw error
555
732
  }
556
733
  }
@@ -569,40 +746,12 @@ export class UniversalWalletConnector {
569
746
  message: string,
570
747
  options?: OperationOptions
571
748
  ): Promise<SignatureType> {
572
- const session = this.sessionManager.getSession()
573
-
574
- if (!session.activeWallet) {
575
- throw new Error('No active wallet')
576
- }
577
-
578
- if (!session.connectionMode) {
579
- throw new Error('No active connection')
580
- }
581
-
582
749
  try {
583
- // Get the provider from the active wallet based on connection mode
584
- let provider
585
- if (session.connectionMode === 'injected') {
586
- provider = this.usingIntegratedBrowser
587
- ? session.activeWallet.integratedBrowserInjectedProvider
588
- : session.activeWallet.extensionInjectedProvider
589
- } else if (session.connectionMode === 'walletConnect') {
590
- provider = session.activeWallet.walletConnectProvider
591
- } else if (session.connectionMode === 'tonConnect') {
592
- return await this.signatureService.signMessage(
593
- message,
594
- undefined,
595
- options
596
- )
597
- }
598
-
599
- if (!provider) {
600
- throw new Error('No provider available for the active wallet')
601
- }
602
-
603
- return await this.signatureService.signMessage(message, provider, options)
750
+ // Validation + funnel instrumentation live in SignatureService (after
751
+ // validation, at the real wallet boundary) — same layering as connect.
752
+ return await this.signatureService.signMessage(message, options)
604
753
  } catch (error) {
605
- this.emitError(error, 'signMessage')
754
+ this.emitError(error, 'signMessage', options?.signal)
606
755
  throw error
607
756
  }
608
757
  }
@@ -616,34 +765,13 @@ export class UniversalWalletConnector {
616
765
  * @returns A promise that resolves to the 65-byte hex signature (0x-prefixed)
617
766
  */
618
767
  async signTypedData(typedData: EIP712TypedData): Promise<string> {
619
- const session = this.sessionManager.getSession()
620
-
621
- if (!session.activeWallet) {
622
- throw new Error('No active wallet')
623
- }
624
-
625
- if (!session.connectionMode) {
626
- throw new Error('No active connection')
627
- }
628
-
629
- if (session.connectionMode === 'tonConnect') {
630
- throw new Error('signTypedData is not supported for TON wallets')
631
- }
632
-
633
- // Resolve the injected provider from the active wallet. WalletConnect
634
- // resolves its own provider inside the connector, so it stays undefined here.
635
- let provider
636
- if (session.connectionMode === 'injected') {
637
- provider = this.usingIntegratedBrowser
638
- ? session.activeWallet.integratedBrowserInjectedProvider
639
- : session.activeWallet.extensionInjectedProvider
640
-
641
- if (!provider) {
642
- throw new Error('No provider available for the active wallet')
643
- }
768
+ try {
769
+ // Validation + funnel instrumentation live in SignatureService.
770
+ return await this.signatureService.signTypedData(typedData)
771
+ } catch (error) {
772
+ this.emitError(error, 'signTypedData')
773
+ throw error
644
774
  }
645
-
646
- return await this.signatureService.signTypedData(typedData, provider)
647
775
  }
648
776
 
649
777
  /**
@@ -656,44 +784,12 @@ export class UniversalWalletConnector {
656
784
  request: TransactionRequest,
657
785
  options?: OperationOptions
658
786
  ): Promise<TransactionResult> {
659
- const session = this.sessionManager.getSession()
660
-
661
- if (!session.activeWallet) {
662
- throw new Error('No active wallet')
663
- }
664
-
665
- if (!session.connectionMode) {
666
- throw new Error('No active connection')
667
- }
668
-
669
787
  try {
670
- // Get the provider from the active wallet based on connection mode
671
- let provider
672
- if (session.connectionMode === 'injected') {
673
- provider = this.usingIntegratedBrowser
674
- ? session.activeWallet.integratedBrowserInjectedProvider
675
- : session.activeWallet.extensionInjectedProvider
676
- } else if (session.connectionMode === 'walletConnect') {
677
- provider = session.activeWallet.walletConnectProvider
678
- } else if (session.connectionMode === 'tonConnect') {
679
- return await this.transactionService.sendTransaction(
680
- request,
681
- undefined,
682
- options
683
- )
684
- }
685
-
686
- if (!provider) {
687
- throw new Error('No provider available for the active wallet')
688
- }
689
-
690
- return await this.transactionService.sendTransaction(
691
- request,
692
- provider,
693
- options
694
- )
788
+ // Validation + funnel instrumentation live in TransactionService (after
789
+ // validation, at the real wallet boundary) — same layering as connect.
790
+ return await this.transactionService.sendTransaction(request, options)
695
791
  } catch (error) {
696
- this.emitError(error, 'sendTransaction')
792
+ this.emitError(error, 'sendTransaction', options?.signal)
697
793
  throw error
698
794
  }
699
795
  }
@@ -704,25 +800,15 @@ export class UniversalWalletConnector {
704
800
  * tonConnect doesn't implement it.
705
801
  */
706
802
  async signSolanaTransaction(serializedTx: Uint8Array): Promise<Uint8Array> {
707
- const session = this.sessionManager.getSession()
708
-
709
- if (!session.activeWallet) {
710
- throw new Error('No active wallet')
711
- }
712
-
713
- if (!session.connectionMode) {
714
- throw new Error('No active connection')
715
- }
716
-
717
- // The active connector must implement the optional signSolanaTransactionBytes.
718
- const connector = this.connectors.get(session.connectionMode)
719
- if (!connector?.signSolanaTransactionBytes) {
720
- throw new Error(
721
- `signSolanaTransaction is not supported for connection mode "${session.connectionMode}"`
803
+ try {
804
+ // Validation + funnel instrumentation live in TransactionService.
805
+ return await this.transactionService.signSolanaTransactionBytes(
806
+ serializedTx
722
807
  )
808
+ } catch (error) {
809
+ this.emitError(error, 'signSolanaTransaction')
810
+ throw error
723
811
  }
724
-
725
- return await connector.signSolanaTransactionBytes(serializedTx)
726
812
  }
727
813
 
728
814
  /**
@@ -744,7 +830,7 @@ export class UniversalWalletConnector {
744
830
  options
745
831
  )
746
832
  } catch (error) {
747
- this.emitError(error, 'getWalletCapabilities')
833
+ this.emitError(error, 'getWalletCapabilities', options?.signal)
748
834
  throw error
749
835
  }
750
836
  }
@@ -809,22 +895,73 @@ export class UniversalWalletConnector {
809
895
  return session.activeWalletCapabilities
810
896
  }
811
897
 
898
+ /**
899
+ * Set the console log threshold at runtime (e.g. from a support toggle). The
900
+ * global `window.UWC_DEBUG` override still takes precedence for ad-hoc triage.
901
+ */
902
+ setLogLevel(level: LogLevel): void {
903
+ this.logger.setLevel(level)
904
+ }
905
+
906
+ /**
907
+ * Release observer subscriptions and clear all event listeners. Call on full
908
+ * teardown / logout to avoid leaking listeners across connector lifecycles.
909
+ */
910
+ dispose(): void {
911
+ this.observerTeardown?.()
912
+ this.observerTeardown = undefined
913
+ // Release any live provider-lifecycle listeners on a connected session —
914
+ // `dispose()` can be called without a prior `disconnect()`, and leaving the
915
+ // EIP-1193/adapter `.on` handlers attached is the zombie-listener leak this
916
+ // method's contract promises to prevent.
917
+ this.connectionService.teardownLifecycle()
918
+ this.eventManager.clearAllListeners()
919
+ // Full teardown: if this was the shared singleton, clear the static refs too
920
+ // so a later getInstance()/new starts clean — otherwise getInstance() would
921
+ // hand back this now-listener-less instance, and the instance-count warning
922
+ // would mis-fire on the next construction.
923
+ if (UniversalWalletConnector.instance === this) {
924
+ UniversalWalletConnector.resetInstance()
925
+ }
926
+ }
927
+
812
928
  // ---- private helpers ----
813
929
 
814
- private emitError(error: unknown, operation: UWCOperation): void {
930
+ private emitError(
931
+ error: unknown,
932
+ operation: UWCOperation,
933
+ signal?: AbortSignal
934
+ ): void {
815
935
  // Intentional cancellations (AbortSignal.throwIfAborted, fetch abort,
816
936
  // DOMException) should NOT surface as wallet errors — the caller already
817
- // knows it asked to abort. Skip emission so error telemetry stays clean.
818
- if (error instanceof Error && error.name === 'AbortError') {
937
+ // knows it asked to abort. Uses the same name+signal check as
938
+ // `instrumentHandoff`, so both a DOMException abort (not an `Error`
939
+ // subclass in every runtime) AND a custom `abort(reason)` — whose rejection
940
+ // value carries the caller's own name — are skipped consistently.
941
+ if (isAbortError(error) || signal?.aborted) {
819
942
  return
820
943
  }
821
- const walletError: WalletError =
822
- error instanceof WalletConnectorError
823
- ? { type: error.type, message: error.message }
824
- : {
825
- type: 'unknown',
826
- message: error instanceof Error ? error.message : String(error)
827
- }
828
- this.eventManager.emit('error', { error: walletError, operation })
944
+ // Classify here too so a PRE-wallet validation failure — which never enters
945
+ // the handoff funnel (no awaitingWallet/terminal) — still carries
946
+ // failureKind/source/errorCode in telemetry. The #1 prod connection error,
947
+ // `no_common_network`, lands here; forward its two CAIP-2 id lists.
948
+ const { errorCode, failureKind, source } = classifyError(error)
949
+ this.eventManager.emit('error', {
950
+ error: toWalletError(error),
951
+ operation,
952
+ errorCode,
953
+ failureKind,
954
+ source,
955
+ // A handoff terminal already reported this failure — mark this record as
956
+ // the back-compat duplicate so metrics can count terminals + uncovered
957
+ // errors without double-counting.
958
+ ...(hasHandoffTerminal(error) && { handoffCovered: true }),
959
+ ...(error instanceof NoCommonNetworkError && {
960
+ networkMismatch: {
961
+ walletNetworkIds: error.walletNetworkIds,
962
+ configuredNetworkIds: error.configuredNetworkIds
963
+ }
964
+ })
965
+ })
829
966
  }
830
967
  }
@@ -0,0 +1,15 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { generateId } from './id'
3
+
4
+ describe('generateId', () => {
5
+ it('produces unique values', () => {
6
+ const ids = new Set(Array.from({ length: 1000 }, () => generateId()))
7
+ expect(ids.size).toBe(1000)
8
+ })
9
+
10
+ it('produces a non-empty string', () => {
11
+ const id = generateId()
12
+ expect(typeof id).toBe('string')
13
+ expect(id.length).toBeGreaterThan(0)
14
+ })
15
+ })
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Generate a correlation id (RFC-4122 v4 when available).
3
+ *
4
+ * Used for the per-instance `sdkSessionId` and per-handoff `handoffId` — both are
5
+ * opaque correlation tokens, never derived from wallet data (no address, no key).
6
+ * Prefers the native `crypto.randomUUID()` (browsers + Node ≥ 16); falls back to a
7
+ * `crypto.getRandomValues`-backed v4, and finally to a timestamp + monotonic
8
+ * counter only in environments that expose neither. Correlation ids need to be
9
+ * collision-resistant enough to pair a start event with its terminal — NOT
10
+ * cryptographically unpredictable — so the last-resort branch uses no PRNG.
11
+ */
12
+ // Monotonic counter for the no-crypto fallback: guarantees per-process uniqueness
13
+ // without any pseudo-random generator (correlation ids need uniqueness, not
14
+ // unpredictability — and a counter is strictly more collision-resistant here).
15
+ let fallbackCounter = 0
16
+
17
+ export function generateId(): string {
18
+ const c = typeof globalThis !== 'undefined' ? globalThis.crypto : undefined
19
+
20
+ if (c?.randomUUID) {
21
+ return c.randomUUID()
22
+ }
23
+
24
+ if (c?.getRandomValues) {
25
+ const bytes = c.getRandomValues(new Uint8Array(16))
26
+ // Set the version (4) and variant (10xx) bits per RFC 4122 §4.4.
27
+ bytes[6] = (bytes[6]! & 0x0f) | 0x40
28
+ bytes[8] = (bytes[8]! & 0x3f) | 0x80
29
+ const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0'))
30
+ return (
31
+ hex.slice(0, 4).join('') +
32
+ '-' +
33
+ hex.slice(4, 6).join('') +
34
+ '-' +
35
+ hex.slice(6, 8).join('') +
36
+ '-' +
37
+ hex.slice(8, 10).join('') +
38
+ '-' +
39
+ hex.slice(10, 16).join('')
40
+ )
41
+ }
42
+
43
+ // Last-resort fallback (no Web Crypto). Timestamp + monotonic counter keeps
44
+ // ids unique within the process without a PRNG. Sufficient for correlation.
45
+ return (
46
+ 'uwc-' + Date.now().toString(36) + '-' + (fallbackCounter++).toString(36)
47
+ )
48
+ }