@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
package/README.md CHANGED
@@ -121,11 +121,11 @@ await connector.connect('injected', 'metamask', 'eip155:1')
121
121
 
122
122
  Three mutually exclusive ways a user can connect:
123
123
 
124
- | Mode | Description | Typical wallets |
125
- | --------------- | ------------------------------------------------------------------ | -------------------------------- |
124
+ | Mode | Description | Typical wallets |
125
+ | --------------- | ----------------------------------------------------------------------------------------------------- | -------------------------------------- |
126
126
  | `injected` | Browser-extension or in-app injected provider (EIP-6963, Solana Wallet Standard, Tron, TON JS Bridge) | MetaMask, Phantom, Tonkeeper extension |
127
- | `walletConnect` | WalletConnect v2 relay + QR code / deeplink | Any WC-compatible mobile wallet |
128
- | `tonConnect` | TON Connect JS Bridge + QR code / universal link | Tonkeeper, OKX TON Wallet |
127
+ | `walletConnect` | WalletConnect v2 relay + QR code / deeplink | Any WC-compatible mobile wallet |
128
+ | `tonConnect` | TON Connect JS Bridge + QR code / universal link | Tonkeeper, OKX TON Wallet |
129
129
 
130
130
  A wallet can support multiple modes. `isConnectionModeAvailable(mode, walletId)`
131
131
  tells you which ones are actually viable in the current runtime.
@@ -141,7 +141,7 @@ interface Session {
141
141
  activeNetwork: Network | null
142
142
  activeAddress: string | null
143
143
  publicKey: string | null
144
- availableNetworks: Network[] // networks the current wallet supports
144
+ availableNetworks: Network[] // networks the current wallet supports
145
145
  availableAddresses: AvailableAddress[] // one per chain the wallet exposed
146
146
  activeWalletCapabilities: Record<string, EVMCapabilities> | null
147
147
  activeNetworkWalletCapabilities: EVMCapabilities | null
@@ -153,33 +153,44 @@ a `sessionChanged` event fires.
153
153
 
154
154
  ### Observer / event model
155
155
 
156
- The connector is an event emitter. There are twelve typed events:
157
-
158
- | Event | When it fires |
159
- | ----------------------- | ------------------------------------------------------------ |
160
- | `ready` | Initial wallet detection has completed |
161
- | `walletsDetected` | Detection completed, payload is the enriched wallet list |
162
- | `connecting` | `connect()` started |
163
- | `connectionUri` | A WC/TonConnect pairing URI is now available |
164
- | `connected` | `connect()` finished successfully |
165
- | `disconnected` | `disconnect()` finished (session cleared) |
166
- | `sessionChanged` | Any session field changed |
167
- | `networkSwitching` | Network switch started or ended |
168
- | `networkSwitched` | Network switch finished successfully |
169
- | `capabilitiesUpdated` | `getWalletCapabilities()` refreshed data |
170
- | `error` | A user-initiated op threw |
171
- | `change` | Catch-all — fires after every other event |
172
-
173
- Use `on(eventName, listener)` for targeted updates. Every typed event also
156
+ The connector is an event emitter. The state-change events are:
157
+
158
+ | Event | When it fires |
159
+ | --------------------- | -------------------------------------------------------- |
160
+ | `ready` | Initial wallet detection has completed |
161
+ | `walletsDetected` | Detection completed, payload is the enriched wallet list |
162
+ | `connecting` | `connect()` started |
163
+ | `connectionUri` | A WC/TonConnect pairing URI is now available |
164
+ | `connected` | `connect()` finished successfully |
165
+ | `disconnected` | `disconnect()` finished (session cleared) |
166
+ | `sessionChanged` | Any session field changed |
167
+ | `networkSwitching` | Network switch started or ended |
168
+ | `networkSwitched` | Network switch finished successfully |
169
+ | `capabilitiesUpdated` | `getWalletCapabilities()` refreshed data |
170
+ | `error` | A user-initiated op threw |
171
+ | `change` | Catch-all — fires after every other state-change event |
172
+
173
+ Plus a set of **telemetry-only** events used for logging and wallet-handoff
174
+ visibility (see [Logging & observability](#logging--observability)). These
175
+ deliberately do **not** cascade to `change` (so they never trigger re-renders):
176
+ `log`, `awaitingWallet`, `walletSucceeded`, `walletRejected`, `walletFailed`,
177
+ `walletTimedOut`, `walletDiscoveryFailed`, `bridgeError`, `relayError`,
178
+ `walletAccountChanged`, `walletChainChanged`, `walletDisconnected`,
179
+ `walletSessionExpired`, `lifecycleTelemetryUnavailable`,
180
+ `pageHiddenDuringHandoff`, `pageVisibleDuringHandoff` — the full list is the
181
+ `TELEMETRY_ONLY_EVENTS` export in `events.ts`.
182
+
183
+ Use `on(eventName, listener)` for targeted updates. Every state-change event also
174
184
  cascades to `change`, so the legacy `subscribe(listener)` pattern still works.
175
185
 
176
186
  ### AbortSignal
177
187
 
178
188
  Every async operation accepts an optional `{ signal }`. If the signal aborts,
179
- the operation rejects with an `AbortError` and no session mutations happen
180
- after the abort, even if the wallet prompt has already returned. Wallet
181
- prompts themselves cannot usually be cancelled the signal protects your app
182
- from acting on stale results.
189
+ the operation rejects with the signal's `reason` a `DOMException` named
190
+ `AbortError` by default, or whatever you passed to `controller.abort(reason)`
191
+ and no session mutations happen after the abort, even if the wallet prompt has
192
+ already returned. Wallet prompts themselves cannot usually be cancelled — the
193
+ signal protects your app from acting on stale results.
183
194
 
184
195
  ---
185
196
 
@@ -296,8 +307,8 @@ const connector = new UniversalWalletConnector(
296
307
  )
297
308
  ```
298
309
 
299
- Constructing a **second** instance logs a `console.error` warning it's
300
- almost always a mistake. For tests and full-logout flows, call
310
+ Constructing a **second** instance logs an error through the configured logger
311
+ — it's almost always a mistake. For tests and full-logout flows, call
301
312
  `UniversalWalletConnector.resetInstance()` to clear the cached reference.
302
313
 
303
314
  ---
@@ -307,15 +318,15 @@ almost always a mistake. For tests and full-logout flows, call
307
318
  All reads are synchronous and cheap. Call them any time.
308
319
 
309
320
  ```ts
310
- connector.getSession() // current Session
311
- connector.getState() // { session } — parity with older API
312
- connector.isReady() // true once detection completed
313
- connector.getWallets() // wallets with `installed` flags set
314
- connector.getNetworks() // the configured networks array
321
+ connector.getSession() // current Session
322
+ connector.getState() // { session } — parity with older API
323
+ connector.isReady() // true once detection completed
324
+ connector.getWallets() // wallets with `installed` flags set
325
+ connector.getNetworks() // the configured networks array
315
326
  connector.isConnectionModeAvailable('walletConnect', 'metamask')
316
- connector.getConnectionURI() // WC/TonConnect pairing URI, if any
327
+ connector.getConnectionURI() // WC/TonConnect pairing URI, if any
317
328
  connector.getNetworkSwitchLoadingState()
318
- // { isLoading, isWaitingForUserApproval }
329
+ // { isLoading, isWaitingForUserApproval }
319
330
  connector.getActiveWalletCapabilities() // Record<NetworkId, EVMCapabilities> | null
320
331
  ```
321
332
 
@@ -363,18 +374,21 @@ connector.off('connectionUri', handler)
363
374
  import type { UWCEventMap } from '@meshconnect/uwc-core'
364
375
 
365
376
  // Narrowed per event:
366
- connector.on('connecting', ({ connectionMode, walletId }) => {})
367
- connector.on('connected', ({ session }) => {})
368
- connector.on('disconnected', () => {})
377
+ connector.on('connecting', ({ connectionMode, walletId }) => {})
378
+ connector.on('connected', ({ session }) => {})
379
+ connector.on('disconnected', () => {})
369
380
  connector.on('connectionUri', ({ uri, connectionMode }) => {})
370
- connector.on('networkSwitching', ({ isLoading, isWaitingForUserApproval }) => {})
371
- connector.on('networkSwitched', ({ network }) => {})
372
- connector.on('sessionChanged', ({ session }) => {})
381
+ connector.on(
382
+ 'networkSwitching',
383
+ ({ isLoading, isWaitingForUserApproval }) => {}
384
+ )
385
+ connector.on('networkSwitched', ({ network }) => {})
386
+ connector.on('sessionChanged', ({ session }) => {})
373
387
  connector.on('capabilitiesUpdated', ({ capabilities }) => {})
374
- connector.on('walletsDetected', ({ wallets }) => {})
388
+ connector.on('walletsDetected', ({ wallets }) => {})
375
389
  connector.on('ready', () => {})
376
390
  connector.on('error', ({ error, operation }) => {})
377
- connector.on('change', () => {}) // catch-all; no payload
391
+ connector.on('change', () => {}) // catch-all; no payload
378
392
  ```
379
393
 
380
394
  ### Legacy `subscribe()`
@@ -473,8 +487,8 @@ await connector.switchNetwork('eip155:8453') // Base
473
487
  // Track loading for UI
474
488
  connector.on('networkSwitching', ({ isLoading, isWaitingForUserApproval }) => {
475
489
  if (isWaitingForUserApproval) showBanner('Approve in your wallet…')
476
- else if (isLoading) showBanner('Switching network…')
477
- else hideBanner()
490
+ else if (isLoading) showBanner('Switching network…')
491
+ else hideBanner()
478
492
  })
479
493
  ```
480
494
 
@@ -614,9 +628,10 @@ connectButton.addEventListener('click', () => {
614
628
  cancelButton.addEventListener('click', () => controller.abort())
615
629
  ```
616
630
 
617
- When the signal aborts, the in-flight promise rejects with an `AbortError`.
618
- Session state is **not** mutated after the abort, even if the wallet prompt
619
- later returns a result. This is especially useful in component lifecycles:
631
+ When the signal aborts, the in-flight promise rejects with the signal's
632
+ `reason` (an `AbortError` unless you aborted with a custom one). Session state
633
+ is **not** mutated after the abort, even if the wallet prompt later returns a
634
+ result. This is especially useful in component lifecycles:
620
635
 
621
636
  ```ts
622
637
  // Single-page app: abort on route change
@@ -647,8 +662,8 @@ interface WalletError {
647
662
  ```
648
663
 
649
664
  - `rejected` — user dismissed the prompt.
650
- - `expired` — WalletConnect pairing proposal timed out.
651
- - `unknown` — everything else (network errors, bad wallet state, …).
665
+ - `expired` — WalletConnect pairing proposal timed out.
666
+ - `unknown` — everything else (network errors, bad wallet state, …).
652
667
 
653
668
  ### Per-call handling
654
669
 
@@ -670,26 +685,29 @@ try {
670
685
  ```ts
671
686
  import type { UWCOperation } from '@meshconnect/uwc-core'
672
687
 
673
- connector.on('error', ({ error, operation }: {
674
- error: WalletError
675
- operation: UWCOperation
676
- }) => {
677
- analytics.track('wallet.error', {
678
- operation,
679
- type: error.type,
680
- message: error.message
681
- })
682
- })
688
+ connector.on(
689
+ 'error',
690
+ ({ error, operation }: { error: WalletError; operation: UWCOperation }) => {
691
+ analytics.track('wallet.error', {
692
+ operation,
693
+ type: error.type,
694
+ message: error.message
695
+ })
696
+ }
697
+ )
683
698
  ```
684
699
 
685
700
  `operation` is one of:
686
- `'connect' | 'disconnect' | 'switchNetwork' | 'signMessage' | 'sendTransaction' | 'getWalletCapabilities' | 'initialize'`.
701
+ `'connect' | 'disconnect' | 'switchNetwork' | 'signMessage' | 'signTypedData' | 'sendTransaction' | 'signSolanaTransaction' | 'getWalletCapabilities' | 'initialize'`.
687
702
 
688
703
  ### AbortError
689
704
 
690
- Aborted operations reject with a `DOMException` whose `.name === 'AbortError'`.
691
- They are **not** routed through the `error` event aborts are not wallet
692
- failures.
705
+ Aborted operations reject with the signal's `reason`: a `DOMException` whose
706
+ `.name === 'AbortError'` by default, or your own value when you called
707
+ `controller.abort(customReason)`. Either way they are **not** routed through
708
+ the `error` event — UWC consults the signal itself (not just the error name),
709
+ so even a custom-reason abort is treated as a cancellation, not a wallet
710
+ failure.
693
711
 
694
712
  ```ts
695
713
  try {
@@ -705,6 +723,139 @@ try {
705
723
 
706
724
  ---
707
725
 
726
+ ## Logging & observability
727
+
728
+ UWC ships **no** third-party telemetry SDK. Instead it exposes two opt-in seams
729
+ you wire to whatever stack you run (Datadog, Amplitude, Segment, a plain
730
+ console). With neither configured, there is **zero external egress** — the
731
+ security-first default for a sign-only wallet layer.
732
+
733
+ ### Logging
734
+
735
+ A pluggable, level-gated logger backs UWC's internal logging. Configure it via
736
+ `UWCConfig`:
737
+
738
+ ```ts
739
+ const connector = new UniversalWalletConnector({
740
+ networks,
741
+ wallets,
742
+ logLevel: 'info', // console threshold; default 'warn'
743
+ logger: myAppLogger // optional: redirect output into your own logger
744
+ })
745
+ ```
746
+
747
+ `logger` (any object with `debug`/`info`/`warn`/`error`) receives the raw
748
+ `(message, ...args)`; omit it and a console sink is used with an ISO/level prefix.
749
+
750
+ Tune the threshold at runtime — useful when triaging in a wallet's in-app
751
+ browser where you can't redeploy:
752
+
753
+ ```ts
754
+ connector.setLogLevel('debug') // programmatic
755
+ window.UWC_DEBUG = true // or from the console: surfaces everything
756
+ window.UWC_DEBUG = 'info' // or a specific level (only ever lowers it)
757
+ ```
758
+
759
+ ### Observer (telemetry)
760
+
761
+ Implement `UWCObserver` to receive a normalized, PII-safe event stream plus all
762
+ log lines. The vendor code lives in **your** app, never in UWC:
763
+
764
+ ```ts
765
+ import type { UWCObserver } from '@meshconnect/uwc-core'
766
+
767
+ const observer: UWCObserver = {
768
+ onEvent: e => datadogRum.addAction(`uwc.${e.name}`, e), // or amplitude.track / …
769
+ onLog: (level, message, args) =>
770
+ myAppLogger[level](`[uwc] ${message}`, ...args)
771
+ }
772
+
773
+ const connector = new UniversalWalletConnector({ networks, wallets, observer })
774
+ ```
775
+
776
+ > **Keep `onEvent` / `onLog` non-blocking.** They are called synchronously on the
777
+ > wallet-operation path — a terminal event fires between the wallet returning a
778
+ > result and that result reaching your `await`, so a slow handler adds latency to
779
+ > connect / sign / sendTransaction. Queue-backed sinks (Datadog `addAction`,
780
+ > Segment `track`) are already non-blocking; for anything heavy, defer it yourself
781
+ > (`onEvent: e => queueMicrotask(() => …)`).
782
+
783
+ Every `onEvent` record is a `UWCTelemetryEvent` — only routing facets
784
+ (`operation`, `connectionMode`, `namespace`, `chainId`, `walletId`, `durationMs`,
785
+ outcome…) plus a `sdkSessionId` correlation id. By default `sdkSessionId` is a
786
+ generated per-instance id; pass `correlationId` (a string or getter) to align it
787
+ to your own session key (e.g. `link_sess_id`) so UWC events join your existing
788
+ Datadog/RUM telemetry without a separate mapping:
789
+
790
+ ```ts
791
+ new UniversalWalletConnector({
792
+ ...config,
793
+ observer,
794
+ correlationId: () => myLinkSession.id // resolved per-event; getter may be lazy/dynamic
795
+ })
796
+ ```
797
+
798
+ **Structured fields never carry a signing payload or wallet address** — the shape has no slot for
799
+ them. The two free-form surfaces (`error.message` and `onLog` args) additionally
800
+ pass through a key + content scrubber that redacts sensitive keys, EVM hex blobs
801
+ (addresses / signatures / keys), secret URL params, bearer tokens, and Solana
802
+ base58 / TON (`EQ…`/`UQ…`) addresses. This last pass is best-effort and biased
803
+ toward **over**-redaction (some non-secret base58-looking ids/hashes get redacted
804
+ by design). One gap remains: BARE (non-`0x`) hex — a raw hex string is far more
805
+ often a legitimate id/hash than a secret, so only `0x`-prefixed hex is redacted.
806
+ Structured fields never carry addresses regardless.
807
+
808
+ ### Wallet-handoff funnel
809
+
810
+ The moment UWC hands control to a wallet (extension popup, WC deeplink, TON
811
+ sheet) it emits `awaitingWallet`; when the wallet answers it emits exactly one
812
+ terminal — all sharing a `handoffId` and carrying `durationMs`:
813
+
814
+ ```
815
+ awaitingWallet ─┬─ walletSucceeded success
816
+ ├─ walletRejected user said no (4001 / rejected)
817
+ └─ walletFailed any other error
818
+ ```
819
+
820
+ A non-cancelling `walletTimedOut` marker also fires if a non-TON handoff is still
821
+ pending past `walletResponseTimeoutMs` (default 60s, `0` disables). It is purely
822
+ observational — it never rejects, aborts, or mutates state, and is cleared the
823
+ instant the op settles, so a fast op never false-fires it. This turns the
824
+ previously-blind connect/sign/approve window into a per-(chain × wallet × flow)
825
+ timed funnel.
826
+
827
+ > A failed or rejected op emits **both** a handoff terminal
828
+ > (`walletFailed` / `walletRejected`) **and** the back-compat `error` event. The
829
+ > duplicate `error` record carries `handoffCovered: true`, so failure metrics
830
+ > are: **terminals + `error` where NOT `handoffCovered`** (the uncovered ones
831
+ > are pre-wallet validation failures, which have no terminal). Counting
832
+ > terminals and all `error` records double-counts each wallet failure.
833
+ > One documented edge: a wallet that rejects with a bare string/number (not an
834
+ > object) can't be marked — a WeakSet can't hold primitives — so that rare
835
+ > failure double-counts. EIP-1193 requires object errors, and wrapping the
836
+ > thrown value to make it markable would mutate what the caller receives.
837
+
838
+ > **Volume.** A single connect can emit several records (`awaitingWallet`, a
839
+ > terminal, plus any focus / provider-lifecycle events), and
840
+ > `pageHidden`/`pageVisibleDuringHandoff` fire on each visibility toggle while a
841
+ > handoff is pending. If you forward `onEvent` straight to a metered backend
842
+ > (Datadog RUM, etc.), sample or filter by `name` on your side — UWC emits every
843
+ > record and does not sample for you. Provider-lifecycle events are additionally
844
+ > capped at 100 per attachment (`MAX_LIFECYCLE_EVENTS_PER_ATTACHMENT`) so a
845
+ > storming provider cannot drive unbounded egress.
846
+
847
+ ### Teardown
848
+
849
+ Call `dispose()` on full logout to release observer subscriptions, tear down any
850
+ live provider-lifecycle listeners on a connected session, and clear all
851
+ listeners:
852
+
853
+ ```ts
854
+ connector.dispose()
855
+ ```
856
+
857
+ ---
858
+
708
859
  ## Testing
709
860
 
710
861
  The shared singleton can leak state between tests. Reset it in `beforeEach`:
@@ -726,7 +877,9 @@ When unit-testing consumer code, mock the whole module:
726
877
  vi.mock('@meshconnect/uwc-core', () => ({
727
878
  UniversalWalletConnector: class {
728
879
  static getInstance = () => new this()
729
- getSession = () => ({ /* fixture */ })
880
+ getSession = () => ({
881
+ /* fixture */
882
+ })
730
883
  on = () => () => {}
731
884
  once = () => () => {}
732
885
  off = () => {}
@@ -757,12 +910,12 @@ connector.on('sessionChanged', ({ session }) => {
757
910
  session.activeAddress ?? 'not connected'
758
911
  })
759
912
 
760
- document.querySelector('#connect')!.addEventListener('click', () =>
761
- connector.connect('injected', 'metamask')
762
- )
763
- document.querySelector('#disconnect')!.addEventListener('click', () =>
764
- connector.disconnect()
765
- )
913
+ document
914
+ .querySelector('#connect')!
915
+ .addEventListener('click', () => connector.connect('injected', 'metamask'))
916
+ document
917
+ .querySelector('#disconnect')!
918
+ .addEventListener('click', () => connector.disconnect())
766
919
  ```
767
920
 
768
921
  ### Show QR and auto-close modal on pair
@@ -805,7 +958,9 @@ const controller = new AbortController()
805
958
  window.addEventListener('beforeunload', () => controller.abort())
806
959
 
807
960
  try {
808
- const sig = await connector.signMessage('Sign in', { signal: controller.signal })
961
+ const sig = await connector.signMessage('Sign in', {
962
+ signal: controller.signal
963
+ })
809
964
  submitLogin(sig)
810
965
  } catch (error) {
811
966
  if ((error as { name?: string }).name === 'AbortError') return
@@ -828,6 +983,12 @@ export interface UWCConfig {
828
983
  walletConnectConfig?: WalletConnectConfig
829
984
  tonConnectConfig?: TonConnectConfig
830
985
  tronConnectorConfig?: TronConnectorConfig
986
+ // Observability (all optional; defaults preserve current behaviour)
987
+ observer?: UWCObserver // vendor-agnostic telemetry sink; default = no egress
988
+ logger?: Logger // redirect internal logging; default = console
989
+ logLevel?: LogLevel // console threshold; default 'warn'
990
+ walletResponseTimeoutMs?: number // walletTimedOut deadline; default 60_000, 0 disables
991
+ correlationId?: string | (() => string) // align sdkSessionId to your session key
831
992
  }
832
993
 
833
994
  export interface OperationOptions {
@@ -836,9 +997,15 @@ export interface OperationOptions {
836
997
 
837
998
  export type UWCEventName = keyof UWCEventMap
838
999
  export type UWCOperation =
839
- | 'connect' | 'disconnect' | 'switchNetwork'
840
- | 'signMessage' | 'sendTransaction'
841
- | 'getWalletCapabilities' | 'initialize'
1000
+ | 'connect'
1001
+ | 'disconnect'
1002
+ | 'switchNetwork'
1003
+ | 'signMessage'
1004
+ | 'signTypedData'
1005
+ | 'sendTransaction'
1006
+ | 'signSolanaTransaction'
1007
+ | 'getWalletCapabilities'
1008
+ | 'initialize'
842
1009
  ```
843
1010
 
844
1011
  ### Static methods
@@ -879,6 +1046,10 @@ getNetworkSwitchLoadingState(): {
879
1046
  getActiveWalletCapabilities(): Record<string, EVMCapabilities> | null
880
1047
  isConnectionModeAvailable(mode: ConnectionMode, walletId: string): boolean
881
1048
 
1049
+ // Observability
1050
+ setLogLevel(level: LogLevel): void // tune the console threshold at runtime
1051
+ dispose(): void // release observer subs + clear listeners
1052
+
882
1053
  // Events
883
1054
  on<K extends UWCEventName>(event: K, listener: UWCEventListener<K>): () => void
884
1055
  once<K extends UWCEventName>(event: K, listener: UWCEventListener<K>): () => void